Pular para o conteúdo

Domain Events

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.

  • 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.
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
  1. Nome no passado, expresso na Linguagem Ubíqua.
  2. Imutávelrecord é ideal.
  3. Registrado pelo agregado, publicado após persistir com sucesso (evita publicar algo que a transação depois desfez).
  4. Payload mínimo e estável — IDs e dados essenciais, não o agregado inteiro.
  5. 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.

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;
@Service
public 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;
@Component
public 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]
  • Publicar antes de persistir: emitir o evento e a transação falhar depois; use AFTER_COMMIT.
  • Evento no presente/imperativo: ConfirmarPedido (comando) em vez de PedidoConfirmado (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.

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.