Как Domain Events с нуля

Пошаговое руководство по реализации паттерна Domain Events (События предметной области) с нуля в рамках Domain-Driven Design: от идентификации и создания до диспетчеризации и надежной обработки.
В мире Domain-Driven Design (DDD) и сложных бизнес-приложений Domain Events (события предметной области) являются мощным паттерном, который декоплирует логику и делает систему более гибкой и реактивной. Если говорить просто, Domain Event — это факт, что что-то значимое произошло в предметной области вашего приложения. Например, "ЗаказПодтвержден", "ПользовательЗарегистрирован", "СчетОплачен". Это не техническое событие вроде "БазаДанныхОбновлена", а событие на языке бизнеса. Реализация этого паттерна с нуля требует понимания нескольких ключевых принципов и шагов.

Первым шагом является идентификация событий. Это происходит во время тесного collaboration с доменными экспертами. Вы должны задавать вопросы: "Что является важным изменением состояния в вашем бизнес-процессе, о котором должны узнать другие части системы?" Событие всегда именуется в прошедшем времени, так как оно фиксирует уже свершившийся факт. В коде событие — это простой иммутабельный класс (или record в C#), содержащий все данные, релевантные на момент его возникновения. Например, класс `OrderConfirmedEvent` может содержать `OrderId`, `ConfirmationDate` и `CustomerId`.

Второй шаг — определение места возникновения событий. События генерируются внутри агрегатов (Aggregate Roots) — ключевых сущностей, которые контролируют инварианты. Событие — это часть агрегата, но не его прямое состояние. Стандартный подход — добавить в базовый класс агрегата приватную коллекцию (`private List _domainEvents`) для накопления событий, произошедших в ходе одной бизнес-операции. Методы агрегата, изменяющие его состояние, после успешной валидации и применения изменений создают и добавляют событие в эту коллекцию. Например, метод `Confirm()` в агрегате `Order` после установки статуса в `Confirmed` создает экземпляр `OrderConfirmedEvent` и добавляет его в список.

Третий, критически важный шаг — диспетчеризация событий. После того как агрегат сохраняется (например, через репозиторий в базу данных), накопленные события должны быть опубликованы для всех заинтересованных обработчиков (Event Handlers). Здесь возникает архитектурный выбор. Самая простая реализация "с нуля" — это паттерн Mediator в памяти. Вы создаете простой класс `DomainEventDispatcher`, который хранит регистрацию обработчиков для каждого типа события. После сохранения агрегата вы вызываете `DispatchEventsForAggregate(aggregateId)`, который извлекает события из агрегата (через метод `GetAndClearDomainEvents()`) и передает каждый обработчику. Важно: диспетчеризация должна происходить *после* успешного сохранения агрегата, чтобы гарантировать, что событие соответствует персистентному состоянию.

Четвертый шаг — обработка событий. Обработчики — это классы, реализующие интерфейс типа `IHandle`. Их задача — выполнять побочные эффекты в ответ на событие: отправить email уведомление, обновить read-модель для отчетов, инициировать следующий шаг бизнес-процесса или даже вызвать внешний API. Ключевой принцип: обработчики не должны влиять на исходную бизнес-логику агрегата и не должны бросать исключения, которые откатывают основную транзакцию (если только это не критически важно). Для фоновых задач их лучше помещать в очередь.

Пятый шаг — переход к надежной, асинхронной обработке. Наивная реализация в памяти не устойчива к сбоям. Промышленный подход предполагает сохранение событий в той же транзакции, что и агрегат (паттерн "Outbox" или "Transaction log"). События записываются в таблицу в той же БД, а затем фоновый процесс (например, с помощью библиотек типа MassTransit, NServiceBus или собственного воркера) гарантированно доставляет их обработчикам. Это обеспечивает consistency и надежность.

Реализация Domain Events с нуля — это инвестиция в архитектуру. Она позволяет строить системы, где компоненты слабо связаны, бизнес-логика ясна, а новые функции (в виде обработчиков) можно добавлять, минимально затрагивая существующий код. Начав с простого диспетчера в памяти, вы закладываете фундамент для будущего масштабирования до распределенной, событийно-ориентированной архитектуры, где события становятся кровеносной системой всего приложения.
255 2

Комментарии (6)

avatar
z8v0ld33m 02.04.2026
Жду продолжения! Интересно, как правильно обрабатывать события, особенно с учетом транзакций и откатов.
avatar
sneb7s 03.04.2026
Отличное введение в тему! Как раз искал простой пример реализации Domain Events на C# без фреймворков.
avatar
7r7bdc1xld9 04.04.2026
Не упомянули про интеграционные события. Domain Events остаются внутри bounded context, а это важное ограничение.
avatar
rs5ae3 04.04.2026
Спасибо за акцент на бизнес-событиях, а не технических. Это ключ к пониманию DDD для многих разработчиков.
avatar
cigouoi2 04.04.2026
Согласен, что это мощный паттерн, но не переусложнит ли он простой проект? Добавляет много кода для небольших выгод.
avatar
8nux5n 05.04.2026
На практике часто вижу, что события превращаются в спагетти-код. Главное — четко определить границы значимости события.
Вы просмотрели все комментарии