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

03 - Reconstrução de Estado (Replay)

#consultas-temporais#projecoes#reidratacao#replay
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Reconstrução de Estado (Replay)

Replay é a operação fundamental do Event Sourcing: aplicar eventos em ordem, do primeiro ao último, para produzir estado. Toda leitura de agregado é um replay, toda projeção é um replay contínuo, e toda correção de dados em produção é, em essência, um replay bem orquestrado. A propriedade que torna isso confiável é a determinismo: a mesma sequência de eventos aplicada pela mesma função sempre gera o mesmo estado, sem depender de relógio, rede ou aleatoriedade. Dominar replay significa entender onde ele roda, quanto custa, quando executá-lo de novo e como responder perguntas sobre o passado que sistemas tradicionais não conseguem responder. É também onde aparecem os custos do padrão, porque o preço da flexibilidade é lido em milissegundos e em horas de reprocessamento.

Estado como função determinística dos eventos

O modelo mental correto é tratar o estado como o resultado de uma função pura: estado = aplicar(historico). A função de aplicação recebe um evento e muta apenas o estado interno do agregado, sem chamadas de rede, sem consulta ao banco e sem leitura de relógio. Qualquer dependência externa introduzida ali quebra o determinismo e transforma o replay em loteria, com estados diferentes entre ambientes e execuções.

Essa regra tem uma consequência prática imediata nos dados dos eventos. Se a função precisa do valor do dólar na data da operação, esse valor precisa estar gravado no próprio evento, e não buscado na hora do replay. Eventos autossuficientes são o que garante que um replay daqui a cinco anos, com serviços diferentes no ar, produza exatamente o mesmo resultado. Times que descobrem isso tarde pagam com reconstruções inconsistentes e auditorias inconclusivas.

Reidratação do agregado a partir do stream

O fluxo padrão de um comando carrega o agregado lendo o stream inteiro e aplicando cada evento, incluindo a cadeia de upcasters para converter versões antigas do formato. Ao final, o agregado traz o estado atual e a última versão do stream, que será usada como versão esperada na gravação.

// Reidratação clássica: ler o stream e aplicar os eventos em ordem
public Conta carregar(String contaId) {
    List<EventoEnvolto> historico = eventStore.lerStream("conta-" + contaId);
    if (historico.isEmpty()) {
        throw new AgregadoNaoEncontrado(contaId);   // stream vazio significa entidade inexistente
    }

    Conta conta = new Conta(contaId);
    for (EventoEnvolto envolto : historico) {
        conta.aplicar(upcasters.converter(envolto));   // versões antigas viram formato atual
    }

    long ultimaVersao = historico.get(historico.size() - 1).sequencia();
    conta.definirVersaoEsperada(ultimaVersao);   // base do compare-and-set na gravação
    return conta;
}

A reidratação acontece a cada comando, e é por isso que o custo do replay vira tema de produção. Um agregado com vinte eventos reidrata em microssegundos; um com cem mil eventos pode consumir dezenas de milissegundos por operação, multiplicado pela taxa de comandos. O desenho correto não é otimizar a função de aplicação, e sim reduzir a quantidade de eventos lidos, com snapshots e projeções, temas próprios deste módulo.

Projeções como replay contínuo

Uma projeção é um consumidor que aplica replay de forma incremental e persiste o resultado em um formato otimizado para consulta. Ela acompanha o fluxo de eventos, mantém um checkpoint com a posição global já processada e atualiza visões desenhadas para as telas e relatórios do sistema. O estado de consulta, portanto, é um replay materializado, executado uma única vez por evento, e compartilhado por todas as leituras.

O checkpoint é o detalhe que separa uma projeção confiável de uma frágil. Ele precisa ser gravado na mesma transação do efeito aplicado, garantindo que um crash não perca eventos nem os processe duas vezes sem controle. Como reentregas acontecem, o processador de eventos precisa ser idempotente, usando a posição global do evento como chave de deduplicação. Com essas duas garantias, a projeção sobrevive a deploys, crashes e retrabalhos sem divergir do log.

Vale registrar também o que a projeção não é. Ela não é fonte de verdade, porque pode ser apagada e refeita a qualquer momento a partir do log; se a projeção e o log divergem, o log vence sempre. Ela não é o lugar de regras de negócio, porque reproduz efeitos de eventos e não decide comportamento. Manter essas fronteiras claras evita o caminho comum de acumular lógica nas projeções até que elas se tornem impossíveis de reconstruir com confiança.

Replay em produção para correções e auditoria

O uso mais valorizado do replay em produção é corrigir erros de software sem tocar manualmente nos dados. O cenário típico: uma regra de cálculo estava errada durante três semanas, e pedidos do período carregam valores incorretos nas projeções. O time corrige a regra no código, pausa o consumidor afetado e reconstrói a projeção do zero, relendo o log inteiro com a lógica corrigida. Os eventos nunca mudam; o que muda é a interpretação deles, e o estado volta a ser consistente com o código atual.

Esse mesmo mecanismo serve à auditoria. Quando um regulador pergunta como um determinado limite foi calculado, a equipe executa o stream da entidade em um ambiente isolado, passo a passo, com a versão exata do código da época, se necessário. O resultado é uma evidência reprodutível, e não uma reconstrução a partir de memórias e planilhas. Times em setores regulados relatam que essa capacidade reduz semanas de trabalho por auditoria e elimina discussões inconclusivas sobre o que teria acontecido.

Reconstrução do zero com troca segura

Reconstruir uma projeção em produção é uma operação planejada, com passos definidos e reversíveis. O desenho usual evita apagar a visão em uso, porque as telas continuam servindo usuários durante o reprocessamento.

  1. Congelar a projeção atual ou mantê-la rodando em leitura, dependendo da tolerância a atraso durante a operação.
  2. Criar a visão nova em tabelas ou índices separados, com nome de estágio, por exemplo resumo_pedidos_v2.
  3. Reprocessar o log da posição zero com a lógica corrigida, gravando no estágio e atualizando um checkpoint próprio.
  4. Validar o resultado com contagens, somas e consultas de amostra comparando versão antiga e nova.
  5. Trocar a referência das consultas para o estágio novo em um único deploy, e manter a versão antiga disponível por um período de reversão.
  6. Descartar o estágio antigo depois da janela de reversão, encerrando o custo duplo de armazenamento.

Esse roteiro transforma um momento de pânico em procedimento. Times sem ele executam reconstruções improvisadas no meio de incidentes, com tabelas truncadas e rollback impossível; times com ele tratam reconstrução como rotina de manutenção, executada com o mesmo rigor de um deploy.

O custo de leitura e os limites do replay

O custo do replay é linear no tamanho do histórico lido, e ele cobra em três situações: comando com agregado de stream longo, reconstrução de projeção grande e consulta temporal sobre entidades muito antigas. Em todas, a resposta não é acelerar a leitura, e sim diminuir o volume lido por meio de snapshots para agregados e de projeções materializadas para consultas.

Operação Custo dominante Mitigação usual
Comando sobre agregado com stream longo Leitura e aplicação de milhares de eventos Snapshot periódico do agregado
Reconstrução de projeção grande Varredura do log global e escrita em volume Paralelismo por stream e réplicas de leitura
Consulta temporal sobre entidade antiga Leitura de histórico completo até a data Projeções históricas pré-calculadas para perguntas frequentes
Auditoria pontual de uma entidade Leitura única e localizada Execução sob demanda em ambiente isolado, sem otimização

Uma regra prática amadurecida em produção: perguntas frequentes viram projeções, perguntas raras ficam como replay sob demanda. Tentar pré-calcular tudo infla o armazenamento e o tempo de reconstrução; recalcular tudo sob demanda degrada a experiência das telas. O equilíbrio é uma decisão de produto tanto quanto de arquitetura.

Replay incremental versus total

As duas formas de replay coexistem e servem a necessidades diferentes. O incremental roda em tempo real, consumindo cada evento uma única vez e mantendo o estado de consulta fresco; é o modo normal de operação. O total reprocessa tudo a partir da posição zero; é o modo de manutenção, usado quando a lógica da projeção muda ou quando a visão corrompe por um bug já corrigido.

A decisão entre os modos tem impacto operacional direto. Replay incremental é barato e constante, mas carrega a restrição de que mudanças de lógica afetam apenas eventos futuros. Replay total é caro e pontual, mas garante que o passado inteiro reflita a regra atual. Times maduros agendam o modo total em janelas de manutenção, monitoram posição global e lag como métricas de primeira classe, e mantêm o roteiro de reconstrução sempre testado, porque o dia em que ele é necessário nunca é o dia ideal para descobrir que ele não funciona.

Consultas temporais: o estado no passado

Porque o log guarda tudo, o sistema consegue responder como o mundo estava em qualquer instante. A consulta temporal é um replay com corte: aplicar eventos até a sequência ou timestamp informado e devolver o estado resultante. É a resposta exata para disputas de suporte, para conformidade regulatória e para investigar um bug que só aparecia em um estado antigo do sistema.

// Consulta temporal: saldo da conta como estava no instante do corte
public BigDecimal saldoEm(String contaId, Instant corte) {
    BigDecimal saldo = BigDecimal.ZERO;

    for (EventoEnvolto e : eventStore.lerStreamAte("conta-" + contaId, corte)) {
        if (e.tipo().equals("DepositoRealizado")) {
            saldo = saldo.add(e.dados().getBigDecimal("valor"));
        } else if (e.tipo().equals("SaqueRealizado") || e.tipo().equals("TaxaCobrada")) {
            saldo = saldo.subtract(e.dados().getBigDecimal("valor"));
        }
    }

    return saldo;   // derivado apenas de fatos gravados, auditável por construção
}

O mesmo mecanismo responde perguntas de negócio que pareceriam impossíveis em um banco convencional: quais clientes estavam inadimplentes em dezembro passado com a regra da época, qual era o catálogo visível quando o cliente fez a compra, como o saldo evoluiu mês a mês desde a abertura. Cada resposta custa uma varredura, e é por isso que perguntas recorrentes migraram para projeções históricas mantidas em tempo real.

Há um limite conceitual importante: a consulta temporal responde com base nos eventos gravados, e não com base em tudo que aconteceu no mundo. Se um processo externo calculava frete e só o resultado foi gravado, o replay reconstrói o resultado, não o cálculo. Eventos ricos em dados ampliam o alcance dessas consultas, e eventos pobres em dados o restringem, o que reforça a disciplina de autossuficiência descrita no início.

Erros comuns e boas práticas

  • Deixar a função de aplicação chamar serviços externos; o replay passa a depender de disponibilidade alheia e produz resultados divergentes.
  • Corrigir dados editando eventos no store; a correção certa é evento de compensação ou reconstrução de projeção com lógica corrigida.
  • Reconstruir projeções em produção sem janela de degradação planejada; durante o replay, as visões ficam atrasadas e as telas precisam comunicar isso.
  • Executar replays massivos no primário de escrita; use réplicas de leitura ou um cluster dedicado para reprocessamento.
  • Tratar o checkpoint da projeção como opcional; sem ele, cada reinício reprocessa o log inteiro e sobrecarrega o sistema.
  • Assumir que relógio de evento garante ordem; somente a sequência do stream é confiável para ordenação.
  • Reconstruir projeções sem validação comparativa; contagens e somas entre visão antiga e nova detectam erros de lógica antes do swap.

Práticas que consolidam confiança: mantenha testes de propriedade que comprovem o determinismo da função de aplicação; meça e publique a duração de cada replay grande, transformando reprocessamento em operação previsível; automatize a reconstrução de projeções como runbook testado, e não como script improvisado em incidente; e documente quais perguntas temporais são atendidas por projeções, para que o time saiba o que é consulta barata e o que é replay caro.

Sintoma em produção Causa provável Mitigação
Comandos lentos só em contas antigas Reidratação lendo stream enorme a cada operação Snapshot periódico e leitura a partir da versão salva
Projeção divergindo do log após crash Checkpoint gravado fora da transação do efeito Transacionar efeito e checkpoint, com idempotência por posição
Replay gera estado diferente do esperado Função de aplicação não determinística Purificar a função e trazer dependências para dentro do evento
Banco de escrita degradado em reprocessamento Replay massivo no primário Isolar reprocessamento em réplica ou cluster dedicado
Telas exibindo dados inconsistentes após rebuild Swap de visão feito por partes, sem deploy único Trocar referência das consultas em um único corte e manter reversão