Domain Events: comunicação entre bounded contexts

1. Fundamentos dos Domain Events no DDD

1.1. Definição e propósito

Domain Events são registros imutáveis de algo relevante que aconteceu no domínio. No contexto do Domain-Driven Design (DDD), eles capturam fatos de negócio que podem interessar a outros componentes do sistema, especialmente bounded contexts vizinhos. Um evento não é uma instrução ("faça algo"), mas uma notificação ("algo aconteceu").

// Exemplo de Domain Event
{
  "eventId": "e7b8c9d0-1234-5678-9abc-def012345678",
  "aggregateId": "pedido-98765",
  "type": "PedidoCriado",
  "timestamp": "2025-04-10T14:30:00Z",
  "version": 1,
  "payload": {
    "clienteId": "cli-4455",
    "itens": 3,
    "valorTotal": 299.90
  }
}

1.2. Diferença entre Domain Events, Integration Events e Eventos Técnicos

É essencial distinguir os tipos de eventos em uma arquitetura:

  • Domain Events: representam fatos do domínio (ex: PagamentoConfirmado). São modelados com a linguagem ubíqua e carregam significado de negócio.
  • Integration Events: são Domain Events serializados e publicados para outros bounded contexts. Eles podem conter informações adicionais para viabilizar a comunicação entre sistemas.
  • Eventos Técnicos: não têm relação com o domínio (ex: ServidorReiniciado, CacheExpirou). São úteis para operações, mas não devem ser confundidos com eventos de negócio.

1.3. Características essenciais

  • Imutabilidade: uma vez criado, o evento nunca é alterado.
  • Nome no passado: PedidoCriado, EstoqueReservado, FreteCalculado. O nome reflete um fato consumado.
  • Relevância para o negócio: o evento deve ter significado para stakeholders. Se não gera reação em outro contexto, provavelmente não é um Domain Event.

2. O Papel dos Domain Events na Comunicação entre Bounded Contexts

2.1. Desacoplamento temporal e espacial

Quando bounded contexts se comunicam via eventos, eles não precisam estar online simultaneamente. O contexto A publica um evento e segue seu fluxo; o contexto B processa quando estiver disponível. Isso reduz acoplamento e aumenta resiliência.

2.2. Eventos públicos vs. privados

  • Public Events: são contratos entre bounded contexts. Exigem versionamento, documentação e governança.
  • Private Events: são internos a um contexto. Podem evoluir livremente, sem impacto externo.
// Exemplo de contrato público versionado
{
  "eventType": "estoque.v1.EstoqueReservado",
  "schema": "https://api.exemplo.com/schemas/estoque-reservado-v1.json"
}

2.3. Consistência eventual

Eventos não garantem consistência imediata. A comunicação assíncrona implica que, por um período, os contextos podem ter visões diferentes do estado. O sistema deve ser projetado para tolerar essa latência e reconciliar dados quando necessário.

3. Modelagem e Identificação de Domain Events Significativos

3.1. Descoberta com Event Storming

Event Storming é uma técnica colaborativa onde a equipe identifica eventos do domínio em post-its. O processo começa com eventos caóticos, depois são organizados em sequência temporal e agrupados por bounded context.

3.2. Critérios para relevância

Um evento é relevante quando:
- Gera uma reação em outro contexto (ex: PedidoCriado → contexto de estoque reserva itens)
- Representa um ponto de decisão ou mudança de estado importante
- É registrado para auditoria ou rastreabilidade

3.3. Exemplos típicos

// Eventos comuns em um sistema de e-commerce
PedidoCriado
PagamentoConfirmado
EstoqueReservado
NotaFiscalEmitida
EntregaAgendada
CupomAplicado

4. Estrutura e Serialização de Domain Events

4.1. Campos obrigatórios

Todo Domain Event deve conter:
- EventId: identificador único global
- AggregateId: referência ao agregado que gerou o evento
- Timestamp: momento da ocorrência
- Version: número da versão do schema
- Payload: dados específicos do evento

4.2. Schema evolution e versionamento

Eventos evoluem. Estratégias comuns:
- Aditiva: novos campos são opcionais
- Versionamento explícito: PedidoCriadoV1, PedidoCriadoV2
- Schema Registry: ferramentas como Confluent Schema Registry validam compatibilidade

4.3. Estratégias de serialização

// JSON: simples e legível (mais comum)
{ "eventId": "...", "type": "PedidoCriado", ... }

// Avro: compacto e com schema embutido (bom para Kafka)
// Protobuf: eficiente e fortemente tipado (bom para gRPC)

5. Publicação e Entrega de Eventos entre Contextos

5.1. Mecanismos de publicação

  • Message Broker (Kafka, RabbitMQ): ideal para comunicação entre bounded contexts. Kafka é preferido por sua durabilidade e replay de eventos.
  • Event Bus in-memory: útil apenas dentro do mesmo processo (não atravessa contextos).

5.2. Garantias de entrega

  • Pelo menos uma vez: o evento pode ser entregue mais de uma vez. Consumidores devem ser idempotentes.
  • Exatamente uma vez: mais difícil de implementar, mas possível com sistemas como Kafka (transacional) e idempotência.

5.3. Dead letter queues e retry policies

Eventos que falham repetidamente devem ser enviados para uma fila de mensagens mortas (DLQ). Políticas de retry com backoff exponencial evitam sobrecarga.

// Configuração de retry
{
  "maxRetries": 3,
  "backoffBaseMs": 1000,
  "backoffMultiplier": 2,
  "dlqTopic": "eventos-falhos"
}

6. Consumo e Reação a Eventos em Contextos Distintos

6.1. Handlers de eventos

Handlers devem residir na camada de aplicação, não na de domínio. Eles orquestram a lógica de negócio em resposta ao evento recebido.

6.2. Processamento síncrono vs. assíncrono

  • Síncrono: o consumidor processa imediatamente. Pode causar acoplamento temporal.
  • Assíncrono: o evento é enfileirado e processado em lote. Mais resiliente, mas com maior latência.

6.3. Idempotência e deduplicação

Consumidores devem ser idempotentes: processar o mesmo evento duas vezes não deve causar efeitos colaterais. Estratégias:
- Armazenar IDs de eventos já processados
- Usar chaves únicas no banco de dados

// Exemplo de deduplicação
if (eventStore.exists(event.eventId)) {
  return; // evento já processado
}
processEvent(event);
eventStore.save(event.eventId);

7. Tratamento de Erros, Sagas e Consistência Final

7.1. Sagas coreografadas vs. orquestradas

  • Coreografada: cada contexto reage a eventos e publica novos eventos. Mais descentralizada, mas difícil de rastrear.
  • Orquestrada: um coordenador central (saga manager) decide o fluxo. Mais controlada, mas com ponto único de falha.

7.2. Compensação de eventos

Quando um passo falha em uma saga, eventos de compensação são publicados para reverter operações anteriores.

// Exemplo de evento de compensação
{
  "eventId": "...",
  "type": "PagamentoEstornado",
  "compensates": "PagamentoConfirmado",
  "payload": { "transacaoId": "tx-12345" }
}

7.3. Monitoramento de consistência

Ferramentas como auditoria de eventos, health checks e reconciliação periódica detectam eventos perdidos.

8. Boas Práticas e Anti-Patterns na Comunicação por Eventos

8.1. Evitar acoplamento excessivo

Não compartilhe entidades inteiras. Publique apenas os dados necessários para o contexto consumidor.

// Ruim: compartilha a entidade completa
{ "cliente": { "id": 1, "nome": "João", "endereco": {...}, "telefone": "..." } }

// Bom: apenas dados relevantes
{ "clienteId": 1, "nome": "João" }

8.2. Cuidados com granularidade

Eventos muito granulares (ex: ItemAdicionadoAoCarrinho) podem gerar ruído. Eventos muito genéricos (ex: AlteracaoNoSistema) perdem significado de negócio.

8.3. Documentação e governança

Mantenha um catálogo de eventos com:
- Nome, versão, schema, contexto de origem e destino
- Exemplos de payload
- Contrato de compatibilidade

Referências