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.
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.
- Comando
- valida regrasAgregado
- anexa eventoEvent Store
- publicaProjeção
- atualiza visãoRead model
Navegue pelas camadas
Camada 1/5Agregado (escrita)
use ← →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.
Estrutura
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: 200from dataclasses import dataclass
@dataclass(frozen=True)
class ContaAberta: saldo_inicial: int
@dataclass(frozen=True)
class DinheiroDepositado: valor: int
@dataclass(frozen=True)
class DinheiroSacado: valor: int
class ContaBancaria:
def __init__(self):
self.saldo = 0
self.novos = []
# Replay: o estado atual e derivado da sequencia de eventos
@classmethod
def de_eventos(cls, eventos):
conta = cls()
for e in eventos:
conta._aplicar(e)
return conta
def sacar(self, valor: int):
if valor > self.saldo:
raise ValueError("Saldo insuficiente")
self._registrar(DinheiroSacado(valor))
def _registrar(self, e):
self._aplicar(e)
self.novos.append(e) # anexado ao event store, nunca sobrescrito
def _aplicar(self, e):
if isinstance(e, ContaAberta): self.saldo = e.saldo_inicial
if isinstance(e, DinheiroDepositado): self.saldo += e.valor
if isinstance(e, DinheiroSacado): self.saldo -= e.valor
conta = ContaBancaria.de_eventos([ContaAberta(0), DinheiroDepositado(300)])
conta.sacar(100) # saldo derivado: 200Casos 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.
Armadilhas
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.
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.
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.
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
- CQRS (Command Query Responsibility Segregation)Separa o modelo de escrita (commands) do modelo de leitura (queries), podendo usar stores e serviços distintos otimizados para cada lado.
- SagaCoordena uma transação distribuída de longa duração como uma sequência de transações locais, cada uma com uma ação compensatória para desfazer semanticamente em caso de falha.
- ObserverDefine uma dependência um-para-muitos: quando um objeto muda de estado, todos os seus dependentes são notificados automaticamente.