ArquiteturaAvançado9 min de leitura8 seções2005, Data aproximada

Event Sourcing

Em vez de guardar o estado atual, o sistema guarda a sequência imutável de eventos que o produziu — o estado é sempre derivado por replay, e o histórico é a fonte da verdade.

Em 10 segundos

Não guarde o saldo — guarde os lançamentos. Cada mudança vira um evento imutável num log append-only; o estado atual é derivado por replay, e projeções transformam o log nas visões de leitura que você precisar.

Antes de ter nome

A ideia é tão velha quanto a contabilidade de partidas dobradas e o log de transações do banco (WAL): guardar o que aconteceu e derivar o estado depois.

  • #eventos
  • #event-store
  • #replay
  • #projecoes
  • #auditoria
Analogia

O extrato bancário

Seu banco não guarda seu saldo numa caixinha que alguém edita — o saldo É a soma de todos os lançamentos do extrato desde a abertura da conta. Ninguém 'corrige' um lançamento errado apagando a linha: entra um estorno, novo lançamento, e a história continua íntegra. Por isso o banco consegue dizer exatamente quanto você tinha em 14 de março de 2019: o histórico é a verdade, o saldo é só uma conta feita em cima dele.

01Conceito

O problema

Persistência tradicional é destrutiva: cada UPDATE sobrescreve o estado anterior. O sistema sabe QUE o pedido está cancelado, mas não sabe mais se foi o cliente, o antifraude ou um bug — essa informação morreu no UPDATE.

Auditorias coladas depois (tabela de histórico, trigger, log de aplicação) nascem incompletas: capturam o que alguém lembrou de logar, dessincronizam do estado real e não permitem reconstruir o passado com confiança.

A raiz é uma escolha silenciosa que quase ninguém percebe estar fazendo: tratar o estado atual como fonte da verdade e o histórico como subproduto opcional. O Event Sourcing inverte: o histórico é a fonte da verdade, e o estado atual é cache. Contabilidade faz isso há séculos — livro-razão não aceita borracha.

Essa inversão destrava capacidades impossíveis no modelo destrutivo: viagem no tempo (qual era o estado na terça passada?), depuração forense (replay dos eventos exatos que causaram o bug), projeções retroativas (uma visão nova calculada sobre anos de eventos) e um trilho natural de integração — outros serviços assinam o mesmo log.

O par com CQRS é quase obrigatório na prática: replay a cada consulta seria proibitivo, então o lado de escrita anexa eventos ao store enquanto projeções mantêm read models atualizados para a leitura — o event store faz o papel de write store, e a consistência eventual entre os lados vem junto no pacote. Cuidado também para não confundir com 'arquitetura orientada a eventos': usar eventos para comunicação entre serviços não é Event Sourcing; o padrão só existe quando o log É a persistência primária.

Como se encaixa
  1. Comando
  2. valida regrasAgregado
  3. anexa eventoEvent Store
  4. publicaProjeção
  5. atualiza visãoRead model
A escrita só anexa eventos ao log; tudo à direita do event store é derivado — e pode ser apagado e reconstruído por replay a qualquer momento.
02Anatomia

Navegue pelas camadas

Camada 1/5Agregado (escrita)

Camada 1

Agregado (escrita)

valida comandos e emite eventos

Recebe o comando, reconstrói seu estado por replay dos eventos anteriores e valida as regras ('tem saldo?'). Se a regra passa, emite o evento novo — nunca altera nada, só descreve o que aconteceu.

No código

const conta = ContaBancaria.deEventos(store.carregar(id));
conta.sacar(100);

Se violar: se o agregado gravar estado direto (UPDATE), o log deixa de ser completo e todo o resto — replay, auditoria, projeções — perde a garantia.

03Estrutura

Estrutura

Diagrama de classes
04Implementação

Código

type Evento =
  | { tipo: "ContaAberta"; saldoInicial: number }
  | { tipo: "DinheiroDepositado"; valor: number }
  | { tipo: "DinheiroSacado"; valor: number };

class ContaBancaria {
  private constructor(public saldo = 0, private novos: Evento[] = []) {}

  // Replay: o estado atual e derivado da sequencia de eventos
  static deEventos(eventos: Evento[]): ContaBancaria {
    const conta = new ContaBancaria();
    for (const e of eventos) conta.aplicar(e);
    return conta;
  }

  sacar(valor: number): void {
    if (valor > this.saldo) throw new Error("Saldo insuficiente");
    this.registrar({ tipo: "DinheiroSacado", valor });
  }

  private registrar(e: Evento): void {
    this.aplicar(e);
    this.novos.push(e);   // sera anexado ao event store, nunca sobrescrito
  }

  private aplicar(e: Evento): void {
    switch (e.tipo) {
      case "ContaAberta":        this.saldo = e.saldoInicial; break;
      case "DinheiroDepositado": this.saldo += e.valor; break;
      case "DinheiroSacado":     this.saldo -= e.valor; break;
    }
  }

  eventosPendentes(): Evento[] { return this.novos; }
}

const conta = ContaBancaria.deEventos([
  { tipo: "ContaAberta", saldoInicial: 0 },
  { tipo: "DinheiroDepositado", valor: 300 },
]);
conta.sacar(100);   // saldo derivado: 200
05No mundo real

Casos de uso

Ledger de uma fintech

Cenário

Uma instituição de pagamentos precisa provar ao regulador como cada saldo chegou ao valor atual, suportar conciliação com o BACEN e investigar qualquer divergência de centavos meses depois.

Como o padrão entra

Cada movimentação (Pix recebido, tarifa, estorno) é um evento no stream da conta. O saldo é projeção; o extrato é outra; o relatório regulatório, uma terceira. Divergência na conciliação? Replay do stream mostra o filme completo, evento por evento.

Trade-off: Contas de alto volume acumulam milhões de eventos — snapshots e particionamento de streams deixam de ser otimização e viram requisito operacional desde o dia um.

Carrinho e pedidos em e-commerce

Cenário

Um varejista quer entender por que clientes abandonam carrinhos: o que adicionaram, removeram, em que ordem, quanto tempo hesitaram — dados que um carrinho salvo como JSON do estado final não tem.

Como o padrão entra

ItemAdicionado, ItemRemovido, CupomAplicado, CheckoutIniciado viram eventos. O time de dados cria projeções retroativas sobre meses de histórico ('quantos removeram o item ao ver o frete?') sem ter previsto a pergunta quando os eventos foram gravados.

Trade-off: O valor está na análise, não na operação — se ninguém consome o histórico, pagou-se o custo inteiro do padrão para servir um carrinho que um JSON resolveria.

Rastreamento e disputas em logística

Cenário

Uma transportadora enfrenta disputas: 'a caixa chegou violada', 'o entregador não passou aqui'. A posição atual do pacote não responde nada — a sequência de custódias, sim.

Como o padrão entra

Cada scan, transferência de custódia e foto de entrega é um evento imutável no stream da encomenda. Na disputa, o replay reconstitui a cadeia completa: quem tinha o pacote, quando, onde. Projeções alimentam o mapa em tempo real e o SLA por rota.

Trade-off: Eventos nascem em dispositivos offline e chegam fora de ordem — o modelo precisa separar o tempo do fato do tempo da gravação, e as projeções, tolerar eventos atrasados.

Guardar o estado em vez do evento

Compila, roda, passa no code review. E quebra depois.

Existe uma tabela de eventos, o vocabulário está lá, mas o que se grava é uma fotografia do objeto depois da mudança. Você tem o histórico do que ficou, e não do que aconteceu — que é a única coisa que o padrão existia para dar.

// Isto NAO e um evento. E uma foto do estado, com data.
await eventos.inserir({
  tipo: "PedidoAtualizado",
  pedidoId,
  payload: { status: "cancelado", total: 340.0, itens: [...] },
});

// Perguntas que este registro NAO responde:
// - por que foi cancelado? (cliente desistiu? fraude? falta de estoque?)
// - quem cancelou?
// - o total mudou junto ou ja estava assim?
// - qual item saiu, se algum saiu?

// O evento de verdade nomeia o FATO e carrega so o que mudou:
await eventos.inserir({
  tipo: "PedidoCanceladoPorFaltaDeEstoque",
  pedidoId,
  payload: { skuIndisponivel: "ABC-123", canceladoPor: "sistema" },
});

O que quebra

  • Ao investigarO histórico mostra que o status virou 'cancelado' e não diz por quê — a informação que motivou a mudança nunca foi gravada.
  • Numa projeção novaUma pergunta de negócio que surgiu depois não pode ser respondida sobre o passado, porque o dado necessário não está lá.
  • Na auditoriaDois eventos consecutivos com o objeto inteiro obrigam a comparar campo a campo para descobrir o que mudou.

A saída: O evento nomeia um fato do negócio no passado (PedidoCanceladoPorFaltaDeEstoque), não uma transição genérica (PedidoAtualizado), e carrega o que mudou mais o motivo. Se o nome do evento pudesse ser usado para qualquer alteração daquela entidade, ele ainda é estado disfarçado.

07Cuidado

Armadilhas

  1. Versionamento de eventos

    Eventos gravados são para sempre — mas o negócio muda. Quando DinheiroSacado ganha o campo 'canal', os milhões de eventos v1 no log não ganham junto. Você vai conviver com todas as versões históricas: upcasters que convertem v1→v2 no replay, campos opcionais, e disciplina para nunca renomear/remover retroativamente. É o custo mais subestimado do padrão.

  2. GDPR e o direito ao esquecimento

    'Apague meus dados' colide de frente com 'eventos nunca são apagados'. As saídas têm custo: crypto-shredding (dados pessoais cifrados por usuário; apagar a chave torna os eventos ilegíveis), segregar PII fora do log com referência, ou expurgo seletivo que quebra a imutabilidade. Decida ANTES de gravar o primeiro evento — retrofit disso em produção é brutal.

  3. Replay lento sem estratégia de snapshot

    Reconstruir um agregado com 2 milhões de eventos a cada comando não escala — e o problema aparece tarde, quando os streams já cresceram. Defina desde cedo a política de snapshots (a cada N eventos ou por tempo), lembre que snapshot também versiona (mudou a estrutura do estado? invalide e regenere) e teste o replay completo periodicamente: um log que nunca foi reproduzido é um backup que nunca foi restaurado.

08Decisão

Quando usar × quando evitar

Use quando

3
  • O histórico é requisito de negócio ou regulatório: auditoria completa, ledgers financeiros, rastreabilidade ponta a ponta.
  • Você precisa de visões retroativas: estado em qualquer ponto do tempo, projeções novas sobre eventos antigos.
  • O domínio já é orientado a eventos e combina com CQRS — o log alimenta read models e a integração entre serviços.

Evite quando

3
  • CRUDs em que só o estado atual importa — eventos, projeções e versionamento viram custo sem retorno.
  • O time ainda não domina consistência eventual e modelagem de eventos — erros de modelagem ficam gravados para sempre.
  • Forte exigência de apagamento de dados pessoais sem engenharia para crypto-shredding ou segregação de PII.

Testar o que ficou

3 armadilhas de Event Sourcing viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • gitO histórico de commits é o log imutável de eventos; a árvore de trabalho é só a projeção do estado atual, reconstruível a partir do log.
  • Redux: estado = fold de açõesO estado da store é o resultado de reduzir a sequência de actions despachadas — as ações são a verdade, o estado é derivado delas.
  • Kafka como event storeO tópico guarda os eventos em ordem e os consumidores montam suas próprias visões relendo o log desde o começo quando precisam.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Estado = redução dos eventos.
const conta = eventos.reduce(aplicar, Conta.vazia());

O que ele cobra

2 saltos a mais entre a chamada e o efeito.

  • Reconstruir o estado exige reprocessar eventos, o que pede snapshots quando o log cresce
  • Mudar o formato de um evento antigo é caro: o passado é imutável e precisa de versionamento

Não vale se: não há valor no histórico completo nem em auditoria — guardar só o estado atual é muito mais simples de operar.

Onde isto já custou caro

Incidentes reais que este conceito explica.

Costuma ser confundido com

Onde isto aparece

Continue por aqui