Разбор Domain Events: пошаговая инструкция и практические примеры реализации

Глубокий разбор паттерна Domain Events из Domain-Driven Design. Статья содержит пошаговую инструкцию по проектированию, публикации и обработке событий с подробными практическими примерами на TypeScript и объяснением интеграции с очередями сообщений.
В архитектуре современных сложных систем, особенно в контексте Domain-Driven Design (DDD), Domain Events (события предметной области) играют роль нервной системы, оповещая различные части приложения о значимых изменениях. Это не просто техническая реализация паттерна «издатель-подписчик», а способ явно выразить бизнес-процессы на языке домена. Давайте разберем, что такое Domain Events, как их правильно проектировать и реализовывать с практическими примерами.

Шаг первый: понимание сути. Domain Event — это факт, что что-то произошло в домене, и это что-то важно для других частей системы. Ключевое слово — «факт». Событие фиксирует то, что уже случилось, его нельзя отменить. Примеры: `OrderPlaced` (Заказ размещен), `InvoiceIssued` (Счет выставлен), `UserEmailVerified` (Email пользователя подтвержден). Событие именуется в прошедшем времени и содержит данные, релевантные на момент его возникновения.

Шаг второй: проектирование события. Хорошее Domain Event должно быть атомарным, нести минимальный, но достаточный набор данных и быть неизменяемым. Рассмотрим пример на TypeScript для системы электронной коммерции. Событие `OrderPlaced` может выглядеть так:

```
class OrderPlaced implements DomainEvent {
 public readonly occurredOn: Date;
 constructor(
 public readonly orderId: string,
 public readonly customerId: string,
 public readonly totalAmount: number,
 public readonly lineItems: Array
 ) {
 this.occurredOn = new Date();
 }
}
```

Обратите внимание: класс содержит только данные, необходимые для реакции на факт размещения заказа. Мы не включаем сюда всю модель заказа со всеми полями.

Шаг третий: публикация события. Событие должно публиковаться из агрегата (Aggregate Root) в момент совершения значимого действия. Важно публиковать событие *после* того, как изменения в агрегате успешно сохранены (в рамках транзакции), чтобы избежать inconsistency. В чистой архитектуре это часто делается через коллекцию событий внутри агрегата. Пример метода в агрегате `Order`:

```
class Order extends AggregateRoot {
 private _lineItems: OrderLineItem[] = [];
 // ... другие поля

 public place(customerId: string) {
 // Бизнес-логика проверки...
 this.status = OrderStatus.PLACED;
 this.addDomainEvent(new OrderPlaced(
 this.id,
 customerId,
 this.calculateTotal(),
 this._lineItems.map(li => ({ productId: li.productId, quantity: li.quantity }))
 ));
 }

 public getDomainEvents(): DomainEvent[] { /* возврат накопленных событий */ }
 public clearDomainEvents(): void { /* очистка коллекции */ }
}
```

Шаг четвертый: диспетчеризация и обработка. После сохранения агрегата в репозитории, события извлекаются и отправляются в диспетчер (Event Bus). Диспетчер отвечает за доставку события всем зарегистрированным обработчикам (Event Handlers). Обработчик — это use case или сервис, который выполняет побочный эффект в ответ на событие. Продолжим пример: после `OrderPlaced` может быть несколько обработчиков:

  • `SendOrderConfirmationEmailHandler`: отправляет письмо клиенту.
  • `UpdateInventoryHandler`: резервирует товары на складе.
  • `NotifyWarehouseHandler`: отправляет уведомление на склад для сборки.
Каждый обработчик независим и должен быть идемпотентным (повторная доставка одного события не должна вызывать проблемы).

Шаг пятый: интеграция с инфраструктурой. На практике для асинхронной обработки событий между контекстами (Bounded Context) используется брокер сообщений, например, RabbitMQ, Apache Kafka или облачные очереди (AWS SQS, Google Pub/Sub). Событие сериализуется (например, в JSON) и помещается в очередь. Другие микросервисы или модули подписываются на эти очереди. Это позволяет добиться слабой связанности и отказоустойчивости. Важно разработать схему (schema) для событий и версионировать их для обеспечения обратной совместимости.

Шаг шестой: обработка ошибок и компенсирующие действия. Что если обработчик `UpdateInventoryHandler` не смог зарезервировать товар из-за его отсутствия? Простое отбрасывание события недопустимо. Необходима стратегия повторных попыток (retry policy) с экспоненциальной задержкой. В случае персистентной ошибки событие должно попадать в «мертвую букву» очередь (DLQ) для ручного разбора. В сложных сценариях может потребоваться запуск компенсирующей транзакции (Saga), например, отмена заказа и отправка извинительного письма.

Практический пример: система бронирования отелей. Событие `RoomBooked` публикуется при успешном бронировании. Его обработчики: 1) списывают оплату с карты гостя (внешний платежный сервис), 2) блокируют номер в календаре, 3) начисляют бонусные баллы на счет гостя. Если платеж не прошел, запускается сага, которая отменяет блокировку номера и отменяет начисление баллов.

Внедрение Domain Events требует дисциплины и тщательного проектирования, но результат того стоит. Вы получаете систему, где бизнес-процессы явно выражены в коде, компоненты слабо связаны, а возможность масштабирования и добавления новой функциональности без переписывания существующего кода становится реальностью.
474 3

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

avatar
prn7n444zy 01.04.2026
Наконец-то кто-то объясняет DDD без лишней воды. Автору респект!
avatar
3esfq7x 02.04.2026
Есть риск переусердствовать. Не каждое изменение в агрегате должно порождать событие.
avatar
b7vvq991fj9 02.04.2026
Отличное начало! Как раз искал практические примеры реализации событий в DDD.
avatar
z0sk2i 02.04.2026
Жду продолжения! Особенно интересно, как обрабатывать ошибки в асинхронных обработчиках.
avatar
fi1kdskn 03.04.2026
Статья полезная, но не хватает сравнения с интеграционными событиями. В чём ключевая разница?
avatar
p3rwrsbcvl 03.04.2026
Хорошо, что автор начал с философии, а не с кода. Важно сначала понять 'зачем'.
avatar
qm30emyu7o 04.04.2026
Не согласен, что это 'нервная система'. Скорее, это способ уменьшить связность модулей.
avatar
1lkj68wn 04.04.2026
Актуально. Мы как раз внедряем события в нашем микросервисе для уведомлений.
avatar
k85pl6l7rl 05.04.2026
Слишком абстрактно пока. Хотелось бы сразу увидеть код на C# или Java.
avatar
iezs5iy 05.04.2026
Пример с заказом в интернет-магазине был бы идеален для иллюстрации.
Вы просмотрели все комментарии