Что такое доменное событие (Domain Event) в DDD?

«Что такое доменное событие (Domain Event) в DDD?» — вопрос из категории Архитектура, который задают на 28% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Доменное событие (Domain Event) — это неизменяемый объект-значение, который фиксирует факт, произошедший в предметной области (домене) и имеющий значение для бизнеса. Он представляет собой что-то, что уже случилось в прошлом, например, OrderWasPlaced, UserEmailWasChanged или PaymentFailed. События используются для информирования других частей системы о произошедшем изменении, способствуя слабой связанности между агрегатами и контекстами.

Характеристики доменного события:

  • Именование: Имя события — глагол в прошедшем времени, отражающий завершённое действие.
  • Неизменяемость (Immutability): После создания данные события не могут быть изменены.
  • Содержание: Содержит минимальный набор данных, необходимый для обработки события (например, ID агрегата, временная метка, релевантные свойства).
  • Контекст: Привязано к конкретному агрегату, который его породил.

Пример реализации на PHP:

<?php

declare(strict_types=1);

namespace AppDomainOrderEvent;

use AppDomainOrderValueObjectOrderId;
use AppDomainSharedEventDomainEvent;
use DateTimeImmutable;

final class OrderWasPlaced implements DomainEvent
{
    private DateTimeImmutable $occurredOn;

    public function __construct(
        private OrderId $orderId,
        private string $customerEmail,
        private float $totalAmount
    ) {
        $this->occurredOn = new DateTimeImmutable();
    }

    // Геттеры предоставляют доступ к данным события
    public function getOrderId(): OrderId
    {
        return $this->orderId;
    }

    public function getCustomerEmail(): string
    {
        return $this->customerEmail;
    }

    public function getTotalAmount(): float
    {
        return $this->totalAmount;
    }

    public function occurredOn(): DateTimeImmutable
    {
        return $this->occurredOn;
    }
}

Как это работает внутри агрегата (Order):

class Order extends AggregateRoot
{
    private OrderId $id;
    private array $recordedEvents = [];

    public static function place(OrderId $id, string $customerEmail, array $items): self
    {
        $order = new self();
        $order->id = $id;
        // ... логика создания заказа ...

        // 1. После успешного создания агрегата регистрируем событие
        $order->recordThat(new OrderWasPlaced($id, $customerEmail, $order->calculateTotal()));

        return $order;
    }

    private function recordThat(DomainEvent $event): void
    {
        $this->recordedEvents[] = $event;
    }

    // 2. Метод для извлечения и очистки записанных событий (вызывается после сохранения агрегата)
    public function releaseEvents(): array
    {
        $events = $this->recordedEvents;
        $this->recordedEvents = [];
        return $events;
    }
}

Практическое применение: После сохранения агрегата Order в репозитории, все записанные события (OrderWasPlaced) извлекаются и отправляются в шину событий. На них могут реагировать различные обработчики:

  • Отправка email-подтверждения заказа.
  • Обновление аналитической панели о новых продажах.
  • Резервирование товара на складе.

Это позволяет разделить ответственность и избежать прямых вызовов сервисов внутри доменной логики.