DDD: Domain Events - Comunicação entre contextos (Parte 8)
Este é o oitavo artigo da série sobre Domain-Driven Design. Nos artigos anteriores, exploramos os building blocks fundamentais do DDD. Agora chegou o momento de entender como Domain Events promovem comunicação desacoplada entre agregados e bounded contexts, criando arquiteturas orientadas a eventos robustas e escaláveis.
Os Domain Events são fundamentais para resolver um dos maiores desafios do DDD: como manter consistência e coordenação entre diferentes partes do domínio sem criar acoplamento direto.
O que são Domain Events?
Domain Events são fatos que aconteceram no passado e que são importantes para o domínio. Eles representam mudanças significativas no estado dos agregados que outras partes do sistema precisam conhecer e potencialmente reagir.
Como Martin Fowler define:
"Um Domain Event captura a memória de algo interessante que afeta o domínio."
Características dos Domain Events
- Representam o passado - Algo que já aconteceu
- São imutáveis - Uma vez criados, não podem ser alterados
- Contêm dados relevantes - Informações necessárias para os handlers
- Nomeados com verbos no passado - Ex:
PedidoCriadoEvent
,PagamentoProcessadoEvent
- Fazem parte da Ubiquitous Language - Especialistas do domínio os entendem
Por que Usar Domain Events?
1. Desacoplamento entre Agregados
Events permitem que agregados se comuniquem indiretamente, mantendo suas fronteiras bem definidas:
// ❌ Sem Events - Acoplamento direto
class Pedido extends AggregateRoot
{
public function confirmar(EstoqueService $estoqueService, EmailService $emailService): void
{
$this->status = StatusPedido::CONFIRMADO;
// Acoplamento direto com outros contextos
foreach ($this->itens as $item) {
$estoqueService->reservar($item->getProdutoId(), $item->getQuantidade());
}
$emailService->enviarConfirmacao($this->clienteEmail, $this->numero);
}
}
// ✅ Com Events - Desacoplado
class Pedido extends AggregateRoot
{
public function confirmar(): void
{
$this->status = StatusPedido::CONFIRMADO;
// Apenas registra o evento
$this->raiseEvent(new PedidoConfirmadoEvent(
$this->id,
$this->clienteId,
$this->itens->toArray(),
$this->valorTotal,
new \DateTimeImmutable()
));
}
}
2. Single Responsibility Principle
Events ajudam a manter agregados focados em sua responsabilidade principal:
// Handler separado para lógica de estoque
class ReservarEstoqueQuandoPedidoConfirmado
{
public function __construct(
private readonly EstoqueRepository $estoqueRepository
) {}
public function handle(PedidoConfirmadoEvent $event): void
{
foreach ($event->getItens() as $item) {
$estoque = $this->estoqueRepository->buscarPorProduto($item->getProdutoId());
$estoque->reservar($item->getQuantidade());
$this->estoqueRepository->salvar($estoque);
}
}
}
// Handler separado para notificações
class NotificarClienteQuandoPedidoConfirmado
{
public function __construct(
private readonly NotificacaoService $notificacaoService,
private readonly ClienteRepository $clienteRepository
) {}
public function handle(PedidoConfirmadoEvent $event): void
{
$cliente = $this->clienteRepository->buscar($event->getClienteId());
$this->notificacaoService->enviarConfirmacaoPedido(
$cliente->getEmail(),
$event->getPedidoId(),
$event->getValorTotal()
);
}
}
3. Extensibilidade
Novos handlers podem ser adicionados sem modificar código existente:
// Novo requisito: integração com analytics
class RegistrarAnalyticsQuandoPedidoConfirmado
{
public function handle(PedidoConfirmadoEvent $event): void
{
// Enviar dados para sistema de analytics
$this->analyticsService->track('pedido_confirmado', [
'valor' => $event->getValorTotal()->getValor(),
'cliente_id' => $event->getClienteId()->valor,
'timestamp' => $event->getOcorridoEm()->format('c')
]);
}
}
Types de Events: Domain vs Integration
Domain Events
São eventos que ocorrem dentro de um bounded context e são processados sincronamente na mesma transação:
class PedidoConfirmadoEvent
{
public function __construct(
private readonly PedidoId $pedidoId,
private readonly ClienteId $clienteId,
private readonly array $itens,
private readonly Dinheiro $valorTotal,
private readonly \DateTimeImmutable $ocorridoEm
) {}
// Getters...
}
Integration Events
São eventos que cruzam boundaries entre bounded contexts ou microserviços e são processados assincronamente:
class PedidoConfirmadoIntegrationEvent
{
public function __construct(
public readonly string $pedidoId,
public readonly string $clienteId,
public readonly array $itens,
public readonly float $valorTotal,
public readonly string $ocorridoEm,
public readonly int $versao = 1
) {}
public function toArray(): array
{
return [
'pedido_id' => $this->pedidoId,
'cliente_id' => $this->clienteId,
'itens' => $this->itens,
'valor_total' => $this->valorTotal,
'ocorrido_em' => $this->ocorridoEm,
'versao' => $this->versao
];
}
}
Implementação Prática
1. Event Base Classes
abstract class DomainEvent
{
protected \DateTimeImmutable $ocorridoEm;
protected EventId $eventId;
public function __construct()
{
$this->ocorridoEm = new \DateTimeImmutable();
$this->eventId = EventId::gerar();
}
public function getOcorridoEm(): \DateTimeImmutable
{
return $this->ocorridoEm;
}
public function getEventId(): EventId
{
return $this->eventId;
}
}
abstract class IntegrationEvent extends DomainEvent
{
abstract public function getEventName(): string;
abstract public function getPayload(): array;
}
2. Aggregate Root com Events
abstract class AggregateRoot
{
/** @var DomainEvent[] */
private array $events = [];
protected function raiseEvent(DomainEvent $event): void
{
$this->events[] = $event;
}
public function getUncommittedEvents(): array
{
return $this->events;
}
public function clearEvents(): void
{
$this->events = [];
}
}
3. Event Dispatcher
interface EventDispatcher
{
public function dispatch(DomainEvent $event): void;
public function subscribe(string $eventType, callable $handler): void;
}
class InMemoryEventDispatcher implements EventDispatcher
{
/** @var array<string, callable[]> */
private array $handlers = [];
public function subscribe(string $eventType, callable $handler): void
{
$this->handlers[$eventType][] = $handler;
}
public function dispatch(DomainEvent $event): void
{
$eventType = get_class($event);
if (!isset($this->handlers[$eventType])) {
return;
}
foreach ($this->handlers[$eventType] as $handler) {
$handler($event);
}
}
}
4. Unit of Work com Events
class UnitOfWork
{
/** @var AggregateRoot[] */
private array $aggregates = [];
public function __construct(
private readonly EventDispatcher $eventDispatcher,
private readonly \PDO $pdo
) {}
public function register(AggregateRoot $aggregate): void
{
$this->aggregates[] = $aggregate;
}
public function commit(): void
{
$this->pdo->beginTransaction();
try {
// 1. Salvar agregados
foreach ($this->aggregates as $aggregate) {
$this->persist($aggregate);
}
// 2. Despachar events após persistir
$this->dispatchEvents();
$this->pdo->commit();
// 3. Limpar events dos agregados
$this->clearAggregateEvents();
} catch (\Exception $e) {
$this->pdo->rollback();
throw $e;
}
}
private function dispatchEvents(): void
{
foreach ($this->aggregates as $aggregate) {
foreach ($aggregate->getUncommittedEvents() as $event) {
$this->eventDispatcher->dispatch($event);
}
}
}
private function clearAggregateEvents(): void
{
foreach ($this->aggregates as $aggregate) {
$aggregate->clearEvents();
}
}
}
Padrões de Implementação
1. Immediate Dispatch
Events são despachados imediatamente quando raised:
class ImmediateEventDispatcher implements EventDispatcher
{
public function dispatch(DomainEvent $event): void
{
// Processa imediatamente
foreach ($this->getHandlers(get_class($event)) as $handler) {
$handler($event);
}
}
}
Prós:
- Simples de implementar
- Feedback imediato de erros
Contras:
- Efeitos colaterais executam na mesma transação
- Pode causar performance issues
2. Deferred Dispatch
Events são coletados e despachados depois da persistência:
class DeferredEventDispatcher implements EventDispatcher
{
private array $deferredEvents = [];
public function defer(DomainEvent $event): void
{
$this->deferredEvents[] = $event;
}
public function flushEvents(): void
{
foreach ($this->deferredEvents as $event) {
$this->dispatch($event);
}
$this->deferredEvents = [];
}
}
Prós:
- Events só são despachados se a transação principal for bem-sucedida
- Melhor performance
Contras:
- Mais complexo
- Handlers podem falhar após commit
3. Event Store
Events são persistidos e processados posteriormente:
interface EventStore
{
public function append(string $streamId, DomainEvent $event): void;
public function getEvents(string $streamId): array;
}
class DatabaseEventStore implements EventStore
{
public function append(string $streamId, DomainEvent $event): void
{
$sql = "INSERT INTO events (stream_id, event_type, event_data, occurred_at)
VALUES (?, ?, ?, ?)";
$this->pdo->prepare($sql)->execute([
$streamId,
get_class($event),
json_encode($event),
$event->getOcorridoEm()->format('Y-m-d H:i:s')
]);
}
}
Integration Events
Para comunicação entre bounded contexts:
1. Event Publisher
interface IntegrationEventPublisher
{
public function publish(IntegrationEvent $event): void;
}
class RabbitMQIntegrationEventPublisher implements IntegrationEventPublisher
{
public function __construct(
private readonly AMQPChannel $channel
) {}
public function publish(IntegrationEvent $event): void
{
$message = new AMQPMessage(
json_encode($event->getPayload()),
['content_type' => 'application/json']
);
$this->channel->basic_publish(
$message,
'integration_events',
$event->getEventName()
);
}
}
2. Event Handler Bridge
Converte Domain Events em Integration Events:
class PublicarIntegrationEventQuandoPedidoConfirmado
{
public function __construct(
private readonly IntegrationEventPublisher $publisher
) {}
public function handle(PedidoConfirmadoEvent $domainEvent): void
{
$integrationEvent = new PedidoConfirmadoIntegrationEvent(
$domainEvent->getPedidoId()->valor,
$domainEvent->getClienteId()->valor,
$this->mapearItens($domainEvent->getItens()),
$domainEvent->getValorTotal()->getValor(),
$domainEvent->getOcorridoEm()->format('c')
);
$this->publisher->publish($integrationEvent);
}
}
Event Versioning
Integration events precisam de estratégias de versionamento:
1. Additive Changes
// V1
class PedidoConfirmadoIntegrationEvent
{
public function __construct(
public readonly string $pedidoId,
public readonly string $clienteId,
public readonly float $valorTotal
) {}
}
// V2 - Adicionando campo (backward compatible)
class PedidoConfirmadoIntegrationEvent
{
public function __construct(
public readonly string $pedidoId,
public readonly string $clienteId,
public readonly float $valorTotal,
public readonly ?string $cupomDesconto = null // Novo campo opcional
) {}
}
2. Breaking Changes
class PedidoConfirmadoIntegrationEventV2
{
public function __construct(
public readonly string $pedidoId,
public readonly string $clienteId,
public readonly array $valorTotal, // Mudança breaking - agora é objeto com moeda
public readonly int $versao = 2
) {}
}
Testes
1. Testando Events
class PedidoTest extends TestCase
{
public function test_deve_raise_event_quando_pedido_confirmado(): void
{
// Arrange
$pedido = new Pedido($this->criarPedidoValido());
// Act
$pedido->confirmar();
// Assert
$events = $pedido->getUncommittedEvents();
$this->assertCount(1, $events);
$this->assertInstanceOf(PedidoConfirmadoEvent::class, $events[0]);
$event = $events[0];
$this->assertEquals($pedido->getId(), $event->getPedidoId());
}
}
2. Testando Handlers
class ReservarEstoqueQuandoPedidoConfirmadoTest extends TestCase
{
public function test_deve_reservar_estoque_quando_pedido_confirmado(): void
{
// Arrange
$estoqueRepository = $this->createMock(EstoqueRepository::class);
$estoque = $this->createMock(Estoque::class);
$estoqueRepository
->expects($this->once())
->method('buscarPorProduto')
->willReturn($estoque);
$estoque
->expects($this->once())
->method('reservar')
->with(5);
$handler = new ReservarEstoqueQuandoPedidoConfirmado($estoqueRepository);
$event = new PedidoConfirmadoEvent(
PedidoId::fromString('123'),
ClienteId::fromString('456'),
[['produto_id' => 'abc', 'quantidade' => 5]],
Dinheiro::fromFloat(100.0),
new \DateTimeImmutable()
);
// Act
$handler->handle($event);
// Assert - Mocks fazem as assertions
}
}
Boas Práticas
1. Naming
- Domain Events:
SomethingHappenedEvent
(verbo no passado) - Integration Events:
SomethingHappenedIntegrationEvent
2. Event Data
- Inclua apenas dados necessários para os handlers
- Evite objetos complexos - prefira IDs e valores primitivos
- Para Integration Events, use DTOs serializáveis
3. Error Handling
class RobustEventDispatcher implements EventDispatcher
{
public function dispatch(DomainEvent $event): void
{
foreach ($this->getHandlers(get_class($event)) as $handler) {
try {
$handler($event);
} catch (\Exception $e) {
$this->logger->error('Event handler failed', [
'event' => get_class($event),
'handler' => get_class($handler),
'error' => $e->getMessage()
]);
// Decidir se deve continuar ou parar
if ($this->isRetryable($e)) {
$this->scheduleRetry($event, $handler);
}
}
}
}
}
4. Performance
// Use async processing para Integration Events
class AsyncIntegrationEventPublisher implements IntegrationEventPublisher
{
public function publish(IntegrationEvent $event): void
{
// Queue para processamento em background
$this->queue->push(new PublishIntegrationEventJob($event));
}
}
Quando NÃO Usar Events
- Para mudanças simples que não interessam a outros agregados
- Quando performance é crítica e não há necessidade de desacoplamento
- Em sistemas pequenos onde o overhead não compensa
- Para comunicação síncrona obrigatória entre agregados
Conclusão
Domain Events são uma ferramenta poderosa para:
- Desacoplar agregados mantendo consistência
- Implementar side effects de forma elegante
- Criar arquiteturas extensíveis e testáveis
- Facilitar integração entre bounded contexts
A chave está em entender quando usar Domain Events vs Integration Events e escolher a estratégia de dispatch adequada para cada contexto.
No próximo artigo, exploraremos Context Mapping, entendendo como mapear e gerenciar relacionamentos entre bounded contexts.
Referências
- Evans, Eric. "Domain-Driven Design: Tackling Complexity in the Heart of Software"
- Vernon, Vaughn. "Implementing Domain-Driven Design"
- Fowler, Martin. "Domain Event" - martinfowler.com
- Young, Greg. "What is a Domain Event?" - codebetter.com
- Bogard, Jimmy. "A better domain events pattern" - lostechies.com
- Microsoft. "Domain events: Design and implementation" - docs.microsoft.com