Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - Conceito (Armazenar Eventos, não Estado)

#append-only#event-sourcing#eventos#fonte-de-verdade
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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.