02 - Event Store (Armazenamento de Eventos)
video: https://youtu.be/... no frontmatter da nota.Event Store: Armazenamento de Eventos
O event store é o componente que persiste e recupera sequências imutáveis de eventos, e é o único caminho autorizado a escrever em uma arquitetura com Event Sourcing. Ele é otimizado para três operações: acrescentar eventos ao fim de um stream, ler todos os eventos de um stream em ordem e assinar novos eventos à medida que são gravados. Essas operações parecem triviais, mas as garantias por trás delas, como ordenação por stream e detecção de escrita concorrente, são o que torna o padrão seguro em produção. A escolha de onde rodar o event store e como modelar cada registro define limites de throughput, custo e operabilidade do sistema por anos. Trata-se de um componente diferente tanto de um banco relacional convencional quanto de um message broker, e confundir esses papéis é uma das origens mais comuns de problemas em produção.
O papel do event store na arquitetura
O event store ocupa uma posição central e única: tudo que o sistema sabe sobre o passado está lá, e tudo que muda no presente passa por ele. Nenhum serviço atualiza linhas diretamente; comandos produzem eventos, e os eventos são a única forma de mutação. Essa centralidade cria requisitos específicos que um banco de aplicação comum não entrega por padrão: escrita apenas incremental, ordenação determinística por agregado, leitura por faixa de sequência e leitura global ordenada para alimentar projeções e integrações.
Também é papel do event store servir de ponto de assinatura. Consumidores, como projetores de leitura e sistemas externos, acompanham o fluxo global de eventos e mantêm seus próprios checkpoints. Sem esse mecanismo, cada integração precisaria de polling ad hoc, e o Event Sourcing perderia uma de suas maiores vantagens, que é transformar o histórico em canal de integração vivo.
O append-only precisa ser defendido na prática, não apenas no diagrama. Permissões de escrita devem permitir apenas INSERT, sem UPDATE e sem DELETE para o papel da aplicação; correções de erro passam por eventos de compensação, como EstornoRealizado, e nunca por edição do registro original. Times que cedem à tentação de "consertar" um evento errado no banco destruem a propriedade que sustenta replay, auditoria e confiança no histórico, e o dano só é descoberto muito depois, quando algum consumidor reprocessa o log.
Stream por agregado
A unidade de organização é o stream: cada instância de agregado tem o seu, identificado por um nome estável, como conta-7c41 ou pedido-2025-08871. Dentro do stream, cada evento recebe um número de sequência contínuo e sem lacunas, que define a ordem absoluta dos fatos daquela entidade. Duas contas diferentes têm streams independentes, e a ordem entre eventos de streams distintos não é garantida no nível do agregado, apenas no fluxo global de gravação.
Essa divisão tem consequências práticas. A leitura de estado de uma entidade toca um único stream, o que torna a operação barata e previsível. O fluxo global, por sua vez, existe para consumidores que precisam ver todos os eventos em ordem aproximada de gravação, como projeções e feeds de auditoria. Implementações como o EventStoreDB mantêm os dois índices, o por stream e o global, e expõem ambos via API.
A escolha do nome do stream é uma decisão de contrato, não um detalhe. Renomear o padrão de nomenclatura depois obriga a migração de metadados ou a manutenção de dois nomes em paralelo, e consumidores externos passam a depender da convenção. Times maduros documentam o padrão, como tipo-identificador, e tratam desvios como bug de gravidade alta, porque streams fora do padrão escapam das assinaturas globais baseadas em prefixo.
Leitura por faixa e assinaturas contínuas
Duas formas de consumo coexistem e atendem propósitos distintos. A leitura pontual busca uma faixa de sequência de um stream específico e serve à reidratação de agregados e a consultas forenses. A assinatura contínua acompanha o fluxo e entrega eventos novos assim que gravados, com reconexão automática e confirmação explícita de processamento, o que define a semântica de entrega.
A confirmação explícita merece atenção: se o consumidor confirma antes de persistir o efeito, um crash entre as duas operações perde o evento; se confirma depois, um crash gera reentrega. A resposta correta é aceitar reentrega e tornar o efeito idempotente, geralmente com a posição global do evento como chave de deduplicação. Essa é a mesma decisão de sistemas de mensageria, mas aplicada ao próprio log, o que a torna mais simples de auditar.
Concorrência otimista com versão esperada
Como agregados são carregados na memória e salvos depois, dois processos podem carregar a mesma versão e tentar gravar ao mesmo tempo. O event store resolve isso com concorrência otimista: a escrita declara a versão que espera encontrar no fim do stream e só é aceita se essa versão for a atual. Se outro processo gravou antes, o número não bate, a escrita falha e o comando é reexecutado contra o estado novo. Não há locks de linha e não há fila de espera; há uma decisão clara e um retry.
CREATE TABLE eventos (
stream_id TEXT NOT NULL,
versao BIGINT NOT NULL,
tipo TEXT NOT NULL,
dados JSONB NOT NULL,
metadados JSONB NOT NULL,
gravado_em TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (stream_id, versao)
);
-- A chave primária garante unicidade de versão por stream
INSERT INTO eventos (stream_id, versao, tipo, dados, metadados)
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb);
-- Se outro processo já gravou a mesma versao, a inserção falha
-- por violação de chave; o comando é reexecutado com estado novo
O valor da versão esperada também serve como política de decisão: se o conflito é raro, o retry é transparente; se o conflito é frequente, como em um mesmo catálogo editado por muitos usuários ao mesmo tempo, o sistema precisa de estratégias explícitas, como agregados menores, fusão de intenções ou fila por chave. O importante é que a colisão nunca passe despercebida, como acontece no last-write-wins de bancos convencionais.
Estrutura do registro de evento
Cada registro combina identificação, payload de domínio e metadados de infraestrutura. Manter essas duas últimas camadas separadas evita que contexto técnico polua o contrato de negócio consumido pelas projeções.
{
"eventoId": "3f2b7a11-9c4e-4d5a-b678-1e2f3a4b5c6d",
"eventType": "PedidoPago",
"streamId": "pedido-2025-08871",
"eventNumber": 42,
"contentType": "application/json",
"data": {
"pedidoId": "pedido-2025-08871",
"valor": 129.90,
"formaPagamento": "PIX"
},
"metadata": {
"correlationId": "req-7d1f2c",
"causationId": "cmd-pagar-pedido-11",
"usuario": "maria.souza",
"timestamp": "2025-06-02T14:03:11Z"
}
}
O campo data é o fato de negócio e pertence ao contrato com consumidores. O campo metadata é infraestrutura e muda com mais liberdade, embora também exija disciplina. O eventNumber é atribuído pelo store na gravação e nunca pelo código da aplicação, pois é a fonte da verdade sobre a ordem.
Opções de implementação
| Tecnologia | Pontos fortes | Limitações | Cenário típico |
|---|---|---|---|
| EventStoreDB | Streams nativos, assinaturas, projections embutidas, ACL por stream | Operação exige especialização e comunidade menor que bancos mainstream | Núcleo de Event Sourcing com alto volume e time dedicado |
| PostgreSQL | Equipe já domina, transações sólidas, backup e réplicas maduros | Índices, assinaturas e concorrência precisam ser construídos manualmente | Primeira implementação, equipes pequenas, requisitos moderados |
| DynamoDB | Escala horizontal previsível, custo por uso, TTL integrado | Modelagem do stream e do feed global exige trabalho fino de design | Sistemas serverless em nuvens específicas |
| Kafka | Throughput altíssimo, retenção configurável, ecossistema amplo | Sem streams por agregado nativos, sem verificação de versão esperada por chave | Feed de integração global, não como store de agregados |
O PostgreSQL merece destaque como porta de entrada: uma tabela append-only com chave composta, já mostrada, atende milhares de escritas por segundo com garantias completas. O Kafka, embora seja um log distribuído de qualidade, não substitui o store de agregados sem trabalho adicional, porque a unidade de ordenação dele é a partição e não o agregado, e não existe verificação nativa de versão esperada por entidade. Times usam Kafka com sucesso como camada de publicação depois do store, mantendo os dois papéis separados.
Critérios operacionais pesam tanto quanto os técnicos. O store precisa de backup testado com restauração real, porque é a única cópia da verdade; precisa de réplicas de leitura para isolar varreduras de replay do tráfego de comandos; e precisa de política de arquivamento para streams frios, que em sistemas com muitos anos de operação dominam o volume total. Avalie também a maturidade da observabilidade: métricas de latência de gravação, tamanho de stream e taxa de conflito embutidas economizam meses de instrumentação manual.
Versionamento de eventos com upcasting
Eventos são imutáveis, mas o domínio evolui, e o formato de um fato gravado em 2023 pode não atender ao código de 2026. A resposta não é reescrever o histórico, e sim upcasting: converter eventos de versões antigas para o formato atual no momento da leitura, em memória, sem tocar no store. Cada tipo de evento carrega um número de versão, e uma cadeia de upcasters transforma a versão 1 em 2, e a 2 em 3, até chegar ao formato corrente.
// Upcaster converte ContaAberta v1 para v2 no momento da leitura
public class ContaAbertaV1ParaV2 implements Upcaster {
public EventoEnvolto converter(EventoEnvolto original) {
// V1 guardava nome completo em um campo; V2 separa nome e sobrenome
String nomeCompleto = original.dados().getString("nome");
String[] partes = nomeCompleto.split(" ", 2);
Map<String, Object> dadosV2 = new HashMap<>(original.dados().asMap());
dadosV2.put("nome", partes[0]);
dadosV2.put("sobrenome", partes.length > 1 ? partes[1] : "");
dadosV2.put("canalAbertura", "agencia"); // valor padrão para o passado
return new EventoEnvolto("ContaAberta", 2, dadosV2, original.metadados());
}
}
O padrão mantém a promessa de imutabilidade e dá liberdade de evolução. Regras básicas consolidadas em produção: nunca altere eventos já gravados; trate upcasters como parte do contrato público, com testes próprios; mantenha o número de versão em cada evento gravado; e aceite que eventos muito antigos podem ser lidos apenas por uma cadeia de conversões, nunca direto pelo código atual.
Erros comuns e boas práticas
- Tratar o event store como banco de consulta genérico; consultas devem ir para projeções, e o store deve servir escrita e reidratação.
- Atribuir numeração de eventos na aplicação em vez de no store; a responsabilidade de ordem e unicidade deve ser do componente que grava.
- Ignorar a versão esperada por considerar conflito improvável; sistemas reais têm picos, e o last-write-wins silencioso é um bug latente caro.
- Gravar dados derivados no payload do evento, como saldo após a operação calculado por regra que depois muda; prefira gravar somente o que o fato determina.
- Misturar eventos de agregados diferentes no mesmo stream; a fronteira do stream deve acompanhar a fronteira do agregado.
- Deixar upcasters sem testes isolados; um erro de conversão silencia o histórico inteiro de uma entidade, e o defeito só aparece em leituras de dados antigos.
Boas práticas consolidadas: defina política de retenção e arquivamento desde o início, porque o log cresce sempre; monitore conflitos de concorrência como métrica de primeira classe; use transações do banco subjacente para agrupar múltiplos eventos de um mesmo comando; e mantenha leituras de stream sempre por faixa de sequência, evitando varreduras inteiras em streams longos. Complete o conjunto com alerta de lacuna de sequência, que indica gravação parcial, e com testes de restauração periódicos do backup, pois um store sem restauração verificada não é uma fonte de verdade, é uma aposta.
| Sintoma em produção | Causa provável | Mitigação |
|---|---|---|
| Escritas concorrentes perdendo eventos | Ausência de verificação de versão esperada | Adotar chave composta ou compare-and-set na gravação |
| Projeções atrasando sem erro visível | Falta de assinatura com checkpoint durável | Usar subscriptions do store com posição persistida |
| Leitura de agregado degradando com o tempo | Streams crescentes lidos por inteiro a cada comando | Introduzir snapshots e leitura a partir da versão salva |
| Histórico ilegível para auditoria | Eventos técnicos sem semântica de negócio | Redefinir eventos no vocabulário do domínio e versionar |