Event sourcing sem a cerimônia que a maioria nunca precisou

Event sourcing sem cerimônia não é arquitetura de conferência. A maioria das equipes precisa de um log de eventos append-only, uma tabela de projeção e limites de escrita claros — não clusters Kafka nem middleware CQRS antes de ter usuários.

Engenharia8 min de leitura
Event sourcingArquitetura de softwarePostgresAudit logBackend
Compartilhar

Event sourcing sem cerimônia começa com uma pergunta que muitas revisões de arquitetura pulam: que problema o event log resolve que uma tabela normal não pode? A resposta de manual — histórico completo, consultas temporais, estado reproduzível — é real. A implementação de manual — event store dedicado, pipeline de snapshots, workers de projeção, schemas versionados, orquestração de sagas — costuma ser desproporcional. Equipes adotam o rótulo antes de precisar da maquinaria. Seis meses depois mantêm três write paths, dois message buses e um job de replay em que ninguém confia, tudo para responder "quem mudou o status desta fatura?" — uma pergunta que uma tabela de auditoria append-only responde em quarenta linhas de SQL.

O terreno útil é event sourcing leve: tratar mutações de domínio como eventos imutáveis, projetar estado atual em tabelas otimizadas para consulta, e adiar infraestrutura pesada até que volume de replay, escopo de compliance ou coreografia entre serviços exijam de verdade. O padrão preserva o benefício central — histórico auditável e estado reconstruível — sem pagar o imposto de sistemas distribuídos no dia um.

Event sourcing completo falha quando a cerimônia chega antes da demanda

O event sourcing clássico prescreve um stack. Comandos entram em um agregado. O agregado emite eventos. Eventos chegam a um store append-only. Projeções consomem o stream e constroem read models. Snapshots aceleram o reload do agregado. Process managers coordenam workflows multi-etapa entre agregados. Cada camada resolve um problema real em escala. Cada camada também soma superfície operacional: regras de evolução de schema, consumidores idempotentes, tratamento de poison messages, monitoramento de lag de projeção e runbooks de on-call para falhas de replay.

Equipes pequenas batem no muro quando implementam o stack completo para um único bounded context dentro de um monólito. O write model vira uma ilha event-sourced em um mar CRUD. Desenvolvedores precisam lembrar quais tabelas são projeções (nunca atualizar direto) e quais são legacy (atualizar livremente). Depurar um bug visível ao usuário exige rastrear do read model até o evento que causou a divergência — muitas vezes entre três repos e um Kafka local.

O modo de falha não é teórico. Um módulo de billing event-sourced "por flexibilidade futura" enquanto assinaturas, metering e faturamento seguem em CRUD produz semântica split-brain: reembolsos aparecem no log mas o dashboard lê uma view materializada com cinco minutos de atraso. Tickets de suporte reportam "o sistema diz reembolsado mas o saldo está errado." A equipe passa um sprint construindo infra de snapshots em vez de corrigir o bug de projeção.

Event sourcing sem cerimônia significa que o log é audit trail primeiro e religião arquitetural depois.

O diagnóstico é simples. Se o consumidor principal do stream é um auditor de compliance ou um investigador de incidentes — não uma frota de projeções em tempo real — a equipe precisa de audit log, não event sourcing. Se o consumidor principal é um read model dentro do mesmo deployable, precisa de tabela append-only e função de projeção, não message bus.

Event sourcing leve é log append-only mais projeção

O padrão mínimo viável tem três peças.

Uma tabela de eventos append-only. Cada linha registra o que aconteceu: tipo de evento, identificador do agregado, payload em JSONB, ator, timestamp e opcionalmente sequência monotônica ou hash chain para evidência de tampering. Sem updates. Sem deletes salvo arquivo com retenção governada. Postgres lida com uma tabela, índices BRIN em occurred_at para intervalos temporais e índices GIN em chaves extraídas do payload para buscas por entidade.

Uma projeção para o estado atual. Uma função — trigger, worker em background ou passo inline na transação — aplica cada evento a uma tabela otimizada para consulta. InvoiceCreated insere uma linha. InvoicePaid atualiza status e paid_at. A tabela de projeção é o que a aplicação lê. A tabela de eventos é o que investigadores e jobs de replay leem.

Um limite de escrita. Código de aplicação nunca muta tabelas de projeção exceto aplicando eventos. Essa regra é imposta por convenção em um monólito (writers privados ao módulo) ou por permissões de banco em setups mais estritos. Um ponto de entrada emite eventos; um caminho de código os aplica.

CREATE TABLE domain_events (
  id            BIGSERIAL PRIMARY KEY,
  aggregate_id  UUID NOT NULL,
  event_type    TEXT NOT NULL,
  payload       JSONB NOT NULL,
  actor_id      UUID,
  occurred_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
 
CREATE INDEX domain_events_aggregate_idx
  ON domain_events (aggregate_id, occurred_at);
 
CREATE TABLE invoices (
  id          UUID PRIMARY KEY,
  status      TEXT NOT NULL,
  amount_cents INTEGER NOT NULL,
  paid_at     TIMESTAMPTZ
);
 
INSERT INTO domain_events (aggregate_id, event_type, payload, actor_id)
VALUES ($1, 'InvoicePaid', '{"paid_at": "2026-07-10T12:00:00Z"}', $2);
 
UPDATE invoices
SET status = 'paid', paid_at = '2026-07-10T12:00:00Z'
WHERE id = $1;

Isso não é CQRS. Não são microsserviços. É um registro honesto de mutações com superfície de leitura derivada — o mesmo padrão que frameworks de compliance descrevem como audit trail imutável, estendido com estrutura suficiente para reconstruir estado se a projeção derivar.

A arquitetura de monólito modular torna esse padrão natural: a tabela de eventos vive no schema do módulo dono, tabelas de projeção são privadas ao módulo, e outros módulos veem só a API pública de consulta. Limites permanecem enforced pela linguagem até que extração exija rede de verdade.

Três sinais que justificam mais maquinaria

Nem todo sistema para no padrão mínimo. Três sinais indicam que o stack com cerimônia vale o custo.

Replay em escala. Reconstruir projeções do zero deve completar em janela de recuperação definida — horas, não dias. Um milhão de eventos replayed por triggers síncronos não cumpre esse SLA. Snapshot stores e workers de projeção em background se justificam quando replay é operação de produção, não conveniência de desenvolvimento.

Coreografia entre serviços. Múltiplos deployables devem reagir ao mesmo stream com garantias de ordem. Tabela append in-process não coordena serviço de pagamentos, worker de fulfillment e serviço de notificações. Log durável com consumer groups entra quando o limite de rede é real, não antecipado.

Imutabilidade regulatória com prova externa. SOC 2, HIPAA e registros financeiros às vezes exigem evidência de tampering além de permissões de banco. Linhas com hash chain, exports WORM ou ancoragem externa justificam infra adicional quando auditores pedem prova criptográfica, não só controles de acesso.

Até que um desses sinais esteja presente, adicionar Kafka é especulação. Adicionar versionamento de snapshots é especulação. O log leve lida com os primeiros 100k eventos e o primeiro questionário de compliance.

Evolução de schema sem framework de versionamento de eventos

O problema prático mais difícil em event sourcing não é armazenamento — é mudança de schema. Eventos são imutáveis. Payloads escritos há dois anos ainda devem deserializar hoje.

Event sourcing leve lida com evolução por convenções, não frameworks.

Upcast na leitura. Guardar eventos na forma original. Ao replay ou projetar, função pequena migra payloads InvoiceCreatedV1 para a forma atual antes de aplicar. Versão vive em sufixo de event_type ou coluna schema_version.

Mudanças aditivas em voo. Campos novos são opcionais com defaults. Campos removidos permanecem em eventos antigos mas são ignorados por código de projeção novo. Renomeações breaking passam por tipo de evento novo, não mutação in-place.

Rebuilds de projeção como válvula de escape. Se lógica de projeção muda materialmente, truncar tabela de projeção e replay do log. Com menos de um milhão de eventos isso completa em minutos no Postgres. Script de rebuild é o teste de integração para evolução de schema — se replay falha, migração não é segura.

Evitar proliferação prematura de tipos de evento. UserEmailChanged, UserEmailChangedV2 e UserEmailCorrected no mesmo trimestre significa que o modelo de domínio não era estável o suficiente para event sourcing ainda. Corrigir o modelo antes do log.

Quando event sourcing leve vence CRUD mais triggers de auditoria?

A decisão não é ideológica. É operacional.

Qual é o padrão de leitura principal?

Se a aplicação sempre consulta estado atual e ocasionalmente precisa de histórico para suporte, CRUD com trigger de auditoria na mesma tabela é mais simples. Triggers do Postgres capturam old_value e new_value JSONB em cada update — suficiente para "quem mudou esta linha?" Event sourcing leve vence quando histórico é superfície de produto de primeira classe: activity feeds, undo, relatórios temporais ou reconstrução de estado após bugs de projeção.

A equipe consegue impor um único write path?

Event sourcing falha quando desenvolvedores fazem bypass do log e escrevem direto em tabelas de projeção "só desta vez." Monólito modular com módulos package-private pode impor o limite. Codebase sem ownership não pode — e acumulará drift de projeção mais rápido que CRUD com triggers.

Replay precisa ser automatizado ou export de auditoria basta?

Exports de compliance sobre doze meses não exigem infra de replay. Exigem linhas indexadas e imutáveis consultáveis por actor_id e occurred_at. Infra de replay se justifica quando projeções incorretas devem ser reconstruíveis sem cirurgia SQL manual — tipicamente depois que a equipe enviou pelo menos um bug de projeção para produção e sentiu o custo de recuperação.

Um ponto de vista oposto

Um argumento frequente sustenta que event sourcing parcial é pior que nenhum — que equipes devem se comprometer ao padrão completo com tooling adequado ou ficar em CRUD, porque event store pela metade gera mais confusão que audit log disciplinado.

Esse argumento é correto para equipes sem disciplina de enforcement. Tabela de projeção atualizada por três code paths é pior que CRUD honesto. É incorreto para equipes que já impõem limites de módulo e precisam de estado reconstruível dentro de um deployable. Event sourcing leve é o caminho intermediário para bounded contexts onde histórico importa mas message bus ainda não existe.

O modo de falha a evitar é rotular tabelas CRUD como "events" sem semântica append-only. Tabela events com ON UPDATE CASCADE não é event sourcing. É audit log mal nomeado que falhará na primeira auditoria de imutabilidade.

O que vale a pena lembrar

  • Event sourcing sem cerimônia é log append-only, tabela de projeção e um único limite de escrita — não message bus por padrão.
  • Se o consumidor principal do stream é compliance ou investigação de incidentes, começar com audit log.
  • Kafka, snapshots e orquestração de sagas valem o custo em escala de replay, coreografia entre serviços ou requisitos de imutabilidade criptográfica.
  • Evolução de schema usa funções upcast e rebuilds de projeção — não frameworks de versionamento prematuros.
  • Monólito modular é o host natural: um módulo possui log, projeção e API pública de consulta.
  • Event sourcing parcial sem disciplina de write path cria estado split-brain pior que CRUD com triggers.

Conclusão

Event sourcing virou sinônimo de infraestrutura pesada o bastante para precisar de platform team próprio. Essa associação empurrou equipes para adoção heroica ou evitação total. O meio produtivo é menor: registrar o que aconteceu, derivar o que é verdade agora, e adicionar maquinaria quando dor medida — tempo de replay, ordem entre serviços, rigor de auditoria — supera o que Postgres e limite de módulo disciplinado podem carregar.

A próxima revisão de arquitetura não deveria perguntar "devemos event-source?" Deveria perguntar quais mutações precisam de histórico imutável, quais consultas precisam de estado projetado, e se algum sinal no horizonte justifica log durável além do banco que a aplicação já roda. A maioria dos sistemas em estágio inicial responde com uma tabela append-only e regra clara sobre quem pode tocar o read model. Isso é cerimônia suficiente por muito tempo.

Artigos relacionados

Paleta de comandos

Pesquise um comando para executar...