01 - Conceito (Armazenar Eventos, não Estado)
video: https://youtu.be/... no frontmatter da nota.Event Sourcing: Armazenar Eventos, não Estado
Event Sourcing é o padrão de persistência em que a fonte primária de verdade do sistema não é o estado atual das entidades, e sim a sequência completa e imutável de eventos que descreve tudo que aconteceu com elas. Em vez de gravar "saldo igual a 250" em uma coluna, o sistema grava os fatos DepositoRealizado, SaqueRealizado e TaxaCobrada, cada um com seus dados, horário e posição na sequência. O log é append-only: eventos apenas entram, nunca são editados ou removidos. O estado atual vira um derivado, calculado aplicando os eventos em ordem, e pode ser recalculado quantas vezes for necessário. Em produção, o padrão resolve de uma vez demandas que em arquiteturas CRUD exigem mecanismos paralelos frágeis, como auditoria confiável, reprodução de bugs do passado e integração entre sistemas baseada em fatos de negócio.
O problema do estado persistente tradicional
Em um modelo CRUD, cada escrita sobrescreve o valor anterior e o que existia antes desaparece sem rastro. Quando um gestor pergunta por que o saldo mudou de 400 para 120 numa terça-feira, a resposta depende de tabelas de auditoria separadas, triggers de banco ou logs de aplicação, e qualquer um desses artefatos pode estar incompleto ou dessincronizado com o dado real. A atualização destrutiva também cria disputas de concorrência: dois processos leem a mesma linha, calculam valores novos e gravam, e o último escreve por cima do primeiro, perdendo atualização em silêncio. O debugging sofre do mesmo limite, porque o estado observado no incidente já foi substituído várias vezes quando a equipe começa a investigar. Esses sintomas não vêm de implementação ruim, mas de uma decisão estrutural: o banco guarda apenas o presente e trata o passado como problema de outra área.
O efeito aparece em escalas diferentes em cada sistema, mas a raiz é sempre a mesma. Um time descobre que um desconto foi aplicado sem autorização e não tem como provar quem fez nem com quais dados; outro percebe que migrar o cálculo de juros exige reconstruir seis meses de posições que simplesmente não existem mais. Auditorias passam a custar semanas de montagem manual de evidência, e cada integração nova pede consultas ad hoc sobre um histórico que o banco não guarda.
O conceito central do padrão
A inversão proposta é simples de enunciar e profunda nas consequências: o log de eventos é o dado, e todo o resto é derivado. Cada evento representa um fato de negócio que ocorreu, escrito no vocabulário do domínio e no passado, porque já aconteceu e não pode mais ser negado. Eventos são gravados na mesma transação lógica que valida o comando, o que garante que nenhuma mudança de comportamento ocorre sem deixar registro permanente. Dentro de um stream, a ordem é preservada por um número de sequência crescente, e ler o estado é uma redução: aplicar evento por evento sobre um agregado vazio. Como a função de aplicação é determinística, o mesmo stream sempre produz o mesmo estado, e isso permite recalcular qualquer entidade, em qualquer ambiente, quantas vezes for preciso.
O estado atual, portanto, deixa de ser a verdade e passa a ser uma opinião bem fundada sobre o passado. Essa mudança de perspectiva altera discussões técnicas de produto: restabelecer um dado não é mais um procedimento de resgate manual, é um replay; responder "o que o cliente viu na tela ontem às 14h" deixa de ser impossível e vira uma consulta temporal sobre os eventos até aquele instante.
Anatomia de um evento de domínio
Cada evento gravado precisa carregar tudo que o consumidor precisa para reagir sem consultar outras tabelas. O payload de dados descreve o fato em linguagem de negócio; os metadados carregam contexto de infraestrutura, como quem disparou, qual comando originou e qual requisição faz parte.
{
"eventoId": "9f1c8a2e-4b7d-4c3a-8e21-6d5f0a9b3c77",
"tipo": "SaqueRealizado",
"streamId": "conta-7c41",
"sequencia": 4213,
"dados": {
"contaId": "conta-7c41",
"valor": 80.00,
"canal": "mobile"
},
"metadados": {
"correlationId": "b3e2c1d0-1111-4222-8333-444455556666",
"causationId": "cmd-sacar-7777",
"usuario": "joao.silva",
"ocorridoEm": "2025-03-14T18:22:07Z"
}
}
O campo sequencia define a ordem absoluta dentro do stream e é a única referência confiável para reconstrução, já que relógios distribuídos não garantem ordenação. O correlationId amarra eventos que pertencem à mesma interação de usuário e o causationId aponta o comando que os gerou, formando a trilha que a equipe vai usar durante um incidente.
Exemplo prático: conta bancária
// Eventos são fatos imutáveis, nomeados no passado, com dados suficientes para o replay
record DepositoRealizado(String contaId, BigDecimal valor, Instant ocorridoEm) {}
record SaqueRealizado(String contaId, BigDecimal valor, Instant ocorridoEm) {}
record TaxaCobrada(String contaId, BigDecimal valor, String motivo, Instant ocorridoEm) {}
class Conta {
private String id;
private BigDecimal saldo = BigDecimal.ZERO; // estado derivado, nunca persistido
private final List<Object> novosEventos = new ArrayList<>();
// Comando: valida a regra contra o estado atual e decide o evento
public void sacar(BigDecimal valor) {
if (saldo.compareTo(valor) < 0) {
throw new IllegalStateException("Saldo insuficiente para saque");
}
registrar(new SaqueRealizado(id, valor, Instant.now()));
}
// Todo evento entra no fim do log; nada anterior é alterado
private void registrar(Object evento) {
novosEventos.add(evento);
aplicar(evento);
}
// Função determinística: um evento entra, o estado evolui
private void aplicar(Object evento) {
if (evento instanceof DepositoRealizado e) {
saldo = saldo.add(e.valor());
} else if (evento instanceof SaqueRealizado e) {
saldo = saldo.subtract(e.valor());
} else if (evento instanceof TaxaCobrada e) {
saldo = saldo.subtract(e.valor());
}
}
// Reidratação: estado atual nasce de dobrar todo o histórico do stream
public static Conta reidratar(String id, List<Object> historico) {
Conta conta = new Conta();
conta.id = id;
historico.forEach(conta::aplicar);
return conta;
}
}
O saldo não existe em nenhuma coluna: existe na memória enquanto o agregado está em uso e nas projeções que alimentam consultas. Um saque numa conta com histórico de 4.000 eventos não reescreve nada, apenas acrescenta um SaqueRealizado no fim do stream e recalcula o saldo em memória. Se a regra de negócio mudar, por exemplo para cobrar tarifa em saques noturnos, a mudança entra nos comandos, e o histórico permanece intacto e fiel ao que realmente aconteceu na época.
Diferença para um audit log comum
A confusão mais frequente é tratar Event Sourcing como um audit log sofisticado, e a diferença é arquitetural, não cosmética. O audit log é um artefato acessório escrito ao lado da operação real; se o processo cai entre a gravação do dado e a do log, a trilha fica furada e nada no modelo impede isso. No Event Sourcing não existe operação real separada do log: gravar o evento é a transação, e o estado é apenas consequência. O audit log costuma registrar diffs técnicos, como colunas antes e depois, enquanto eventos de domínio carregam semântica, como LimiteAprovado, que um auditor entende sem ler código. Por fim, o audit log não serve para reconstruir estado, porque não foi desenhado com função de aplicação determinística; é exatamente isso que o Event Sourcing entrega.
| Aspecto | CRUD com audit log | Event Sourcing |
|---|---|---|
| Fonte de verdade | Linha atual do registro | Sequência imutável de eventos |
| Transação | Atualiza o dado; log é efeito colateral | Grava o evento; estado é efeito colateral |
| Granularidade do histórico | Diffs técnicos de coluna | Fatos de negócio no vocabulário do domínio |
| Reconstrução de estado | Não suportada de forma confiável | Determinística, por replay do stream |
| Perda de trilha em falha | Possível entre duas escritas | Impossível por construção |
| Consulta do estado atual | Direta e barata | Exige replay ou projeção dedicada |
Implicações diretas em produção
Investigar um incidente deixa de depender de memória e de logs voláteis: a equipe replica o stream do agregado em ambiente de teste e executa os eventos até o instante exato da falha. Auditorias externas passam a ser atendidas com a própria base, sem montagem manual de evidências. Integrações novas consomem eventos já publicados, e o histórico completo fica disponível para novos consumidores sem ETL retroativo. O suporte ganha capacidade de responder disputas com precisão de segundos, porque cada mudança tem autor, comando e horário gravados.
O preço aparece em três frentes. Ler o estado atual exige projeções ou replay, o que adiciona infraestrutura e latência eventual nas consultas. Evoluir o formato dos eventos é responsabilidade permanente do time, porque o log não pode ser reescrito. E o armazenamento cresce sem limite natural, já que nada é sobrescrito, o que exige política de retenção e custos de arquivamento previstos desde o início.
Erros comuns e boas práticas
Erros que aparecem com regularidade em times adotando o padrão:
- Modelar eventos como mudanças genéricas de registro, como ClienteAtualizado com um JSON inteiro, em vez de fatos de negócio; isso destrói a semântica que justifica o padrão.
- Gravar no evento só o ID da entidade e buscar o resto em outras tabelas durante o replay; o evento deve ser autossuficiente para reconstrução sem consultas externas.
- Confundir comando com evento: CancelarPedido é uma intenção que pode ser negada, PedidoCancelado é um fato; misturar os dois corrompe o log com intenções que nunca se concretizaram.
- Esquecer que eventos são contrato público; renomear campos quebra consumidores que já processaram o histórico.
- Usar message broker com retenção finita como event store; o log precisa ser permanente, ordenado por agregado e consultável por stream.
Práticas que evitam a maioria dos problemas:
- Nomeie eventos no passado, com verbo e resultado claros, para que o log seja legível por humanos durante auditoria.
- Mantenha a função de aplicar evento pura, sem relógio, sem I/O e sem aleatoriedade, para que o replay seja confiável entre ambientes.
- Trate o número de sequência como ordenação absoluta dentro do stream; nunca confie em timestamps para ordenar.
- Versione o formato de cada tipo de evento desde o primeiro dia, mesmo sem mudanças previstas, porque o custo de introduzir depois é alto.
| Sintoma em produção | Causa provável | Mitigação |
|---|---|---|
| Replay produz estados diferentes entre ambientes | Função de aplicação depende de relógio ou serviço externo | Isolar toda leitura de contexto fora da aplicação de eventos |
| Auditoria questiona mudança sem rastro | Parte do fluxo grava estado direto no banco | Bloquear escrita direta; todo caminho passa por comandos |
| Consumidores quebram após deploy | Formato de evento alterado sem versionamento | Versionar tipos de evento e aplicar upcasting na leitura |
| Consultas lentas no estado atual | Time lendo estado por replay em tempo real | Manter projeções dedicadas para leitura |
| Log acumula eventos com dado sensível | PII gravada no payload sem política de retenção | Separar PII em referência externa ou usar crypto-shredding |
Comandos, eventos e projeções: papéis distintos
Para evitar confusão conceitual que aparece em todo time iniciante, vale fixar três papéis. O comando expressa uma intenção futura e pode falhar, porque o estado atual pode negar a operação. O evento registra um fato consumado, não aceita negação e nunca falha depois de gravado. A projeção é uma visão derivada, construída consumindo eventos e refeita do zero quando necessário. Um pedido pode receber o comando ConfirmarPedido e gerar zero ou um evento PedidoConfirmado, dependendo das regras; depois de gravado o evento, o mundo mudou de forma definitiva.
Essa separação dá ao time uma gramática comum para discutir comportamento do sistema. Reuniões de design deixam de debater "em qual tabela salvar" e passam a debater quais fatos o sistema deve registrar e quem os consome. O mesmo vocabulário serve para código, para monitoramento e para conversas com a área de negócio, o que reduz o custo de comunicação que em sistemas CRUD costuma ser invisível, mas constante.
Em termos de testes, o ganho é igualmente concreto. Um cenário de teste vira uma lista de eventos de entrada e uma lista de eventos de saída esperados, sem mocks de banco e sem fixtures de estado. Esse estilo de teste, às vezes chamado de given-when-then sobre o log, é determinístico, rápido e expressa regras de negócio diretamente. Times relatam que a suíte de testes de agregado escrita assim sobrevive a refatorações internas sem alteração, porque ela depende apenas da assinatura dos eventos.