Domain Events no Node.js representam fatos relevantes que já aconteceram dentro do negócio. Exemplos incluem PedidoConfirmado, PagamentoAprovado e ClienteBloqueado.
Eventos ajudam a separar o núcleo de domínio de efeitos colaterais como envio de e-mail, atualização de projeções, auditoria e integração com outros contextos. O evento descreve o fato; handlers decidem o que fazer depois.
Evento não é comando
Comandos expressam intenção: ConfirmarPedido. Eventos registram resultado: PedidoConfirmado. Um comando pode falhar; um evento já aconteceu e não deve ser renomeado como ação futura.
Estrutura de um evento
export function pedidoConfirmado({ pedidoId, clienteId, total, occurredAt }) {
return Object.freeze({
id: crypto.randomUUID(),
type: 'PedidoConfirmado',
aggregateId: pedidoId,
version: 1,
occurredAt,
data: { clienteId, total }
});
}Inclua identificador, tipo, aggregate, versão, data e payload mínimo. Evite copiar todo o aggregate.
Registro dentro da entidade
confirmar({ confirmadoEm }) {
if (this.status !== 'rascunho') throw new Error('Estado inválido');
this.status = 'confirmado';
this.#eventos.push(pedidoConfirmado({
pedidoId: this.id,
clienteId: this.clienteId,
total: this.total.centavos,
occurredAt: confirmadoEm
}));
}
removerEventos() {
return this.#eventos.splice(0);
}A entidade registra o fato no mesmo ponto em que a regra muda o estado.
Dispatcher em memória
export class EventBus {
#handlers = new Map();
on(type, handler) {
const list = this.#handlers.get(type) ?? [];
list.push(handler);
this.#handlers.set(type, list);
}
async publish(event) {
for (const handler of this.#handlers.get(event.type) ?? []) {
await handler(event);
}
}
}Esse modelo é útil dentro do processo, mas não garante entrega após falhas.
Quando publicar?
Publicar antes do commit pode gerar um efeito externo mesmo se a transação falhar. Publicar apenas depois do commit pode perder o evento se o processo cair. O padrão Outbox resolve essa lacuna persistindo estado e evento na mesma transação.
Handlers
eventBus.on('PedidoConfirmado', async event => {
await email.enviarConfirmacao(event.data.clienteId);
});Handlers devem ser idempotentes porque mensagens podem ser entregues novamente.
Eventos internos e integração
Um evento de domínio interno pode conter tipos e detalhes próprios do contexto. Um evento de integração deve ter contrato estável para consumidores externos.
const integrationEvent = {
type: 'orders.order-confirmed.v1',
orderId: event.aggregateId,
totalCents: event.data.total,
occurredAt: event.occurredAt
};Use um mapper para evitar acoplar outros sistemas ao modelo interno.
Versionamento
Eventos persistidos ou públicos precisam de versão. Mudanças compatíveis podem adicionar campos opcionais. Mudanças incompatíveis exigem novo tipo ou versão.
Metadados
Metadados úteis incluem:
correlationIdpara acompanhar um fluxo;causationIdpara indicar o evento ou comando anterior;tenantIdem sistemas multi-tenant;traceIdpara observabilidade;- ator responsável pela operação.
Não inclua segredos ou dados pessoais desnecessários.
Ordenação
Ordenação global é cara e muitas vezes desnecessária. Preserve ordem por aggregate ou chave de partição quando a regra depender dela.
Idempotência
async function processar(event) {
const inserted = await inbox.registrarSeNovo(event.id);
if (!inserted) return;
await projeção.aplicar(event);
}O registro do evento processado e o efeito local devem estar na mesma transação quando possível.
Retries e erros
Falhas transitórias podem ser repetidas com backoff. Erros permanentes devem ir para uma dead letter queue ou estado de intervenção. Não faça retry infinito sem observabilidade.
Testes do domínio
pedido.confirmar({ confirmadoEm: new Date('2026-01-01T10:00:00Z') });
const [event] = pedido.removerEventos();
assert.equal(event.type, 'PedidoConfirmado');
assert.equal(event.aggregateId, pedido.id);Testes de handlers
Verifique idempotência, payload ausente, retry e efeitos externos. Em integração, confirme que o evento é gravado junto com o aggregate.
Observabilidade
Registre tipo, ID, aggregate, latência, tentativas e resultado. Métricas devem mostrar backlog, falhas e idade da mensagem mais antiga.
Domain Events e Event Sourcing
Usar eventos de domínio não significa usar Event Sourcing. Em um sistema comum, o estado atual continua em tabelas e os eventos coordenam efeitos. No Event Sourcing, eventos são a fonte principal do estado.
Relação com aggregates
A raiz do aggregate é um bom lugar para registrar eventos porque conhece a transição válida. Leia Aggregates no Node.js.
Erros comuns
- publicar antes do commit;
- usar eventos como chamadas RPC disfarçadas;
- payload enorme;
- sem versionamento;
- handlers não idempotentes;
- depender de ordem global;
- expor modelo interno diretamente;
- não monitorar backlog.
Conclusão
Domain Events no Node.js tornam fatos do negócio explícitos e desacoplam efeitos secundários. O domínio registra o evento, a camada de aplicação garante persistência e handlers executam reações.
Combine eventos com Outbox, idempotência, contratos versionados e observabilidade. Consulte também Domain-Driven Design no Node.js e Audit Logs no Node.js.
Leia o material de Martin Fowler sobre Domain Event e a documentação de eventos do Node.js.




