Domain Events
Definição
Seção intitulada “Definição”Eric Evans adicionou os Domain Events como bloco tático em edições posteriores; Vaughn Vernon os detalhou como peça central da comunicação entre Agregados e Bounded Contexts. Um Domain Event captura algo que ocorreu e que os especialistas de domínio se importam em saber.
Características: nome no passado, imutável (é um fato consumado), carrega os dados mínimos do que aconteceu e um instante de ocorrência.
O problema resolvido
Seção intitulada “O problema resolvido”- Desacoplar agregados: em vez de a transação modificar dois agregados, um agregado publica um evento e outro reage — respeitando “uma transação por agregado” (ver Agregados).
- Integração entre contextos: eventos viajam por mensageria (Kafka/RabbitMQ) e um Bounded Context reage a fatos de outro.
- Consistência eventual: o que não precisa ser imediato acontece de forma assíncrona e resiliente.
Domain Event vs. Integration Event
Seção intitulada “Domain Event vs. Integration Event”| Critério | Domain Event | Integration Event |
|---|---|---|
| Escopo | Interno ao Bounded Context | Entre contextos/serviços |
| Transporte | Em memória (ApplicationEventPublisher) |
Broker (Kafka, RabbitMQ) |
| Acoplamento | Ao modelo interno | Contrato público estável |
| Consistência | Pode ser síncrona na mesma transação | Eventual, assíncrona |
Regras de design
Seção intitulada “Regras de design”- Nome no passado, expresso na Linguagem Ubíqua.
- Imutável —
recordé ideal. - Registrado pelo agregado, publicado após persistir com sucesso (evita publicar algo que a transação depois desfez).
- Payload mínimo e estável — IDs e dados essenciais, não o agregado inteiro.
- Relógio é dependência. O
Instant.now()embutido na factory é cômodo, mas esconde o relógio e dificulta asserções sobre tempo; quando o instante importar no teste, receba-o de fora (Clock/Instant). Ver Testando o Núcleo.
Exemplo em Java + Spring
Seção intitulada “Exemplo em Java + Spring”O agregado acumula eventos; o Application Service os publica após salvar. Listeners reagem — inclusive @TransactionalEventListener para publicar só após o commit.
package com.loja.pedido.domain;
import java.time.Instant;
public record PedidoConfirmado(PedidoId pedidoId, ClienteId clienteId, Instant ocorridoEm) {
public static PedidoConfirmado de(PedidoId pedidoId, ClienteId clienteId) { return new PedidoConfirmado(pedidoId, clienteId, Instant.now()); }}package com.loja.pedido.domain;
import java.util.ArrayList;import java.util.Collections;import java.util.List;
public abstract class AggregateRoot {
private final List<Object> eventos = new ArrayList<>();
protected void registrarEvento(Object evento) { this.eventos.add(evento); }
public List<Object> eventosNaoPublicados() { return Collections.unmodifiableList(eventos); }
public void limparEventos() { this.eventos.clear(); }}package com.loja.pedido.domain;
public class Pedido extends AggregateRoot {
private final PedidoId id; private final ClienteId clienteId; private StatusPedido status;
public Pedido(PedidoId id, ClienteId clienteId) { this.id = id; this.clienteId = clienteId; this.status = StatusPedido.ABERTO; }
public void confirmar() { if (status != StatusPedido.ABERTO) { throw new IllegalStateException("Pedido nao esta aberto"); } this.status = StatusPedido.CONFIRMADO; registrarEvento(PedidoConfirmado.de(id, clienteId)); }
public PedidoId id() { return id; }}package com.loja.pedido.application;
import com.loja.pedido.domain.Pedido;import com.loja.pedido.domain.PedidoId;import com.loja.pedido.domain.PedidoRepository;import org.springframework.context.ApplicationEventPublisher;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;
@Servicepublic class ConfirmarPedidoService {
private final PedidoRepository repository; private final ApplicationEventPublisher publisher;
public ConfirmarPedidoService(PedidoRepository repository, ApplicationEventPublisher publisher) { this.repository = repository; this.publisher = publisher; }
@Transactional public void executar(PedidoId pedidoId) { Pedido pedido = repository.porId(pedidoId) .orElseThrow(() -> new IllegalArgumentException("Pedido inexistente"));
pedido.confirmar(); repository.salvar(pedido);
pedido.eventosNaoPublicados().forEach(publisher::publishEvent); pedido.limparEventos(); }}package com.loja.faturamento.application;
import com.loja.pedido.domain.PedidoConfirmado;import org.springframework.stereotype.Component;import org.springframework.transaction.event.TransactionPhase;import org.springframework.transaction.event.TransactionalEventListener;
@Componentpublic class GerarFaturaAoConfirmarPedido {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void quando(PedidoConfirmado evento) { gerarFatura(evento.pedidoId(), evento.clienteId()); }
private void gerarFatura(Object pedidoId, Object clienteId) { // ... }}sequenceDiagram
participant App as ConfirmarPedidoService
participant Ped as Pedido (Agregado)
participant Repo as PedidoRepository
participant Bus as ApplicationEventPublisher
participant Fat as GerarFaturaAoConfirmarPedido
App->>Ped: confirmar()
Ped->>Ped: registrarEvento(PedidoConfirmado)
App->>Repo: salvar(pedido)
App->>Bus: publishEvent(PedidoConfirmado)
Bus-->>Fat: quando(PedidoConfirmado) [AFTER_COMMIT]
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Publicar antes de persistir: emitir o evento e a transação falhar depois; use
AFTER_COMMIT. - Evento no presente/imperativo:
ConfirmarPedido(comando) em vez dePedidoConfirmado(fato). - Payload gigante: carregar o agregado inteiro em vez de IDs e dados essenciais.
- Acoplar reação síncrona crítica: transformar consistência eventual em dependência forte, quebrando o desacoplamento.
Relações
Seção intitulada “Relações”- Emitidos por Agregados para desacoplar do restante
- Base conceitual: Observer; entrega confiável entre serviços: Message Outbox e Event-Driven Architecture
- Viabilizam consistência eventual entre Agregados e Bounded Contexts
- Nomeados pela Linguagem Ubíqua
- Bloco de Design Estratégico e Tático e parte de Domain-Driven Design
- Voltar para Home
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. Por que o nome de um Domain Event deve estar no passado?
Resposta
Porque representa um fato consumado — algo que já aconteceu (PedidoConfirmado). Nome imperativo indicaria um comando (intenção), que é conceito diferente.
2. Como os Domain Events ajudam a respeitar a regra “uma transação por agregado”?
Resposta
Em vez de alterar dois agregados numa transação, um agregado publica um evento e outro reage depois, alcançando consistência eventual sem acoplar as transações.
3. Por que usar @TransactionalEventListener(AFTER_COMMIT)?
Resposta
Para garantir que o efeito colateral só ocorra se a transação foi realmente confirmada, evitando reagir a um estado que depois é revertido por rollback.
4. Qual a diferença entre Domain Event e Integration Event?
Resposta
O Domain Event é interno ao Bounded Context (geralmente em memória). O Integration Event cruza contextos/serviços via broker (Kafka/RabbitMQ), com contrato público e consistência eventual.