Domain Events

TL;DR

Antes de ser uma mensagem num broker, o evento é um elemento do modelo de domínio: PedidoConfirmado é vocabulário que o especialista de negócio usa, não detalhe de infraestrutura. Tratá-lo assim torna explícito algo que costuma ficar implícito — o instante em que algo aconteceu — e dá um lugar limpo para os efeitos colaterais que hoje se acumulam dentro do método de negócio. A distinção que mais importa, e que quase todo sistema erra na primeira versão: evento de domínio (interno, no vocabulário do modelo, seu) ≠ evento de integração (publicado para fora, contrato público, de todos). Publicar o primeiro cru é como expor suas tabelas.

O dia em que renomear um campo quebrou quatro times

O serviço de pedidos levanta um evento de domínio quando um pedido é confirmado, e alguém teve a ideia prática de publicá-lo direto no broker — afinal, o objeto já existe, já tem os dados, e serializar é uma linha.

Um ano depois, uma refatoração interna renomeia valorLiquido para totalAposDescontos e desmembra endereco em campos separados. É uma mudança interna, do modelo do time de pedidos, sem alteração de comportamento.

No dia do deploy, quatro serviços quebram: faturamento, logística, antifraude e o data lake. Nenhum deles foi consultado, porque ninguém sabia que aquele objeto era um contrato — ele nasceu como classe interna do domínio e virou API pública sem nunca ter sido discutido como tal.

Esse é o custo de não separar as duas coisas. E o mais caro nem é o incidente: é que, a partir dele, o modelo de domínio congela. Ninguém mais quer renomear nada, porque cada refatoração interna virou negociação com quatro times.

O evento como elemento do modelo

A contribuição do padrão, no DDD, é tratar o evento como parte do modelo, não da plumbing. PedidoConfirmado, PagamentoRecusado, EstoqueEsgotado são conceitos que o especialista de negócio já usa nas frases dele — e que, sem o padrão, ficam invisíveis no código, dissolvidos dentro de um método que faz várias coisas.

Duas consequências práticas.

A primeira é que o momento vira explícito. Sem o padrão, o método confirmar() contém a mudança de estado e todos os efeitos que ela dispara: baixar estoque, enviar e-mail, notificar o parceiro, registrar auditoria. O método cresce, mistura níveis, e não há como saber o que é a regra e o que é a consequência. Com o padrão, confirmar() muda o estado e registra que algo aconteceu; os efeitos ficam em ouvintes separados, cada um com uma responsabilidade.

A segunda é que o evento é imutável e nomeado no passado. Ele não pede nada e não pode ser recusado — descreve um fato consumado. Um “evento” que o receptor pode rejeitar era um comando com o nome errado, conforme a nota anterior.

Quando despachar: o detalhe que decide tudo

Se o evento é registrado durante a operação, resta escolher quando os ouvintes rodam — e as duas opções ingênuas têm falhas opostas:

Despachar no meio da transação faz o ouvinte enxergar um estado que ainda não foi confirmado. Se a transação abortar depois, o e-mail já foi enviado anunciando um pedido que não existe. Efeitos externos não têm rollback.

Despachar depois do commit resolve isso — o fato é definitivo quando os ouvintes rodam. Mas abre a outra falha: se o processo cair entre o commit e a publicação, o dado foi gravado e o evento se perdeu, sem erro em lugar nenhum.

O padrão usual é acumular os eventos no agregado durante a operação e despachá-los após o commit, aceitando a segunda falha — que é justamente o dual-write problem que o Outbox existe para resolver, gravando o evento na mesma transação do dado.

Domínio × integração: a fronteira


graph LR
    subgraph INT["Dentro do serviço de Pedidos"]
        A["Agregado Pedido<br/>confirmar()"] -->|"registra"| DE["<b>Evento de domínio</b><br/>PedidoConfirmado<br/>objeto rico · interno · muda quando eu quiser"]
        DE --> H["Ouvintes internos<br/>auditoria · projeção"]
        DE --> T["Tradutor<br/>domínio → contrato"]
    end
    T -->|"publica"| IE["<b>Evento de integração</b><br/>pedido.confirmado.v1<br/>esquema versionado · público"]
    IE --> C1["Faturamento"]
    IE --> C2["Logística"]

    style DE fill:#4A90D9,color:#fff
    style T fill:#F5A623,color:#000
    style IE fill:#4A90D9,color:#fff

O âmbar é a peça que costuma faltar — e é barata: uma função que mapeia o evento interno para o contrato publicado.

Evento de domínioEvento de integração
Alcancedentro do serviçoatravessa fronteiras
Donoo time do domíniocompartilhado com os consumidores
Formaobjeto rico, com tipos do domínioesquema serializável e versionado
Mudaquando o modelo mudasó com compatibilidade e aviso
Transportein-process, síncrono ou nãobroker
Vocabuláriointerno, íntimo do modelopúblico, estável, curado

A regra que evita o incidente da abertura: o evento de domínio nunca sai do serviço sem passar por uma tradução deliberada. O que atravessa a fronteira é um contrato que você escolheu publicar — normalmente menor que o evento interno, e com campos que você aceita sustentar por anos.

O que ele acopla

Pela lente da família: um evento de domínio puramente interno acopla quase nada — é uma reorganização do seu próprio código, e refatorá-lo custa uma busca no repositório.

O acoplamento nasce no instante da publicação, e é a partir daí que ele fica caro. Publicar cru acopla os consumidores ao seu modelo interno, que é a coisa que mais muda num sistema saudável. Publicar um contrato traduzido acopla-os ao contrato, que é a coisa que você projetou para não mudar. O custo dessa segunda via é uma função de tradução e a disciplina de versionar; o custo da primeira é o congelamento do seu domínio.

Vale notar a simetria com a família anterior: é o mesmo raciocínio do DTO — não expor a entidade na fronteira, para que o contrato público e o modelo interno possam evoluir em ritmos diferentes. Aqui o “DTO” é o evento de integração.

Armadilhas comuns

Publicar o evento de domínio cru

O que acontece: o objeto interno é serializado direto no tópico. Meses depois, uma refatoração interna quebra consumidores que ninguém sabia que existiam — e o modelo de domínio congela por medo. Por quê: é a linha de menor esforço no dia em que se escreve, e o custo aparece só quando o modelo precisa mudar. Nada no código sinaliza que aquele objeto virou API pública. Como evitar: uma tradução explícita na fronteira e um esquema versionado. Se você não consegue nomear quem consome um evento publicado, você já tem o problema — só ainda não o encontrou.

Nome imperativo ou no presente

O que acontece: aparecem AtualizarEstoque ou PedidoConfirmando no barramento. Consumidores passam a tratá-los como ordens, e o produtor começa a depender de que alguém obedeça. Por quê: o nome carrega a intenção. Um nome imperativo revela que o produtor sabe o que deve acontecer — e isso é comando, não evento. Como evitar: particípio passado, sempre, e no vocabulário do negócio. Se o nome natural for imperativo, aceite: é um comando, e ele quer uma fila dirigida e um consumidor.

Efeito colateral escondido no ouvinte

O que acontece: um ouvinte de PedidoConfirmado altera outro agregado, que levanta outro evento, que dispara um terceiro ouvinte. Ninguém consegue prever o que uma confirmação faz, e uma falha no meio deixa o sistema num estado inconsistente. Por quê: o ouvinte parece um lugar barato para pendurar comportamento — não exige mudar o código existente, o que é justamente a virtude do padrão levada longe demais. Como evitar: ouvintes devem ser rasos e, preferencialmente, tocar um agregado só. Encadeamento que atravessa vários agregados com regra de negócio no meio é um processo — e processo quer Saga ou Process Manager, com compensação e visibilidade.

Como explicar em inglês

“A domain event is part of the model, not plumbing — PedidoConfirmado is language the business already uses. Making it explicit does two things: it names the moment something happened, and it gives side effects somewhere to live other than inside the business method, which otherwise ends up doing the state change plus five unrelated things. Worth stressing that domain events don’t require a broker — they can be in-process and synchronous, dispatched after commit. The distinction that really matters is domain events versus integration events. A domain event is internal and yours to refactor; an integration event is a public contract shared with consumers. If you publish the raw domain object, you’ve turned your internal model into an API without noticing, and the first time you rename a field you break four teams — and after that nobody dares refactor the domain at all.”

PTEN
evento de domíniodomain event
evento de integraçãointegration event
levantar um eventoto raise an event
despachar após o commitdispatch after commit
linguagem ubíquaubiquitous language
esquema versionadoversioned schema
efeito colateralside effect

O que vem a seguir

Decidido que existe um contrato público a publicar, vem a pergunta que estrutura a família: quanta coisa esse contrato carrega? O primeiro estilo escolhe carregar o mínimo possível.

Veja também

Fontes

  • Eric EvansDomain-Driven Design (2003) e o DDD Reference — Domain Event como bloco de construção do modelo.
  • Martin FowlerDomain Event — a formulação do padrão e a ênfase no fato ocorrido.
  • Vaughn VernonImplementing Domain-Driven Design (2013) — o levantamento de eventos no agregado e o despacho na fronteira transacional.
  • Chris RichardsonDomain event pattern — a versão de microsserviços e a relação com o Outbox.