Event Sourcing: quando o estado atual não é suficiente
1. Por que o estado atual falha?
Sistemas tradicionais baseados em CRUD (Create, Read, Update, Delete) operam sob uma premissa perigosa: o estado atual é suficiente para compreender o sistema. Quando um registro é atualizado, o estado anterior é simplesmente sobrescrito. O banco de dados contém apenas o último valor, não a trajetória que levou até ele.
Considere um sistema bancário. Quando uma conta tem saldo de R$ 1.000, esse número não revela como chegamos lá. Foi um depósito de R$ 500 seguido de outro de R$ 500? Ou um depósito de R$ 10.000 seguido de nove saques? Para auditoria, debugging e conformidade regulatória, essa distinção é crucial.
A diferença fundamental entre estado e eventos é que o estado é uma fotografia congelada, enquanto os eventos são o filme completo. Quando você precisa entender por que um sistema chegou a determinado estado, o estado atual é insuficiente.
2. Fundamentos do Event Sourcing
Event Sourcing propõe uma inversão radical: em vez de armazenar o estado atual, armazenamos cada evento que modificou o estado. Cada evento é imutável e representa um fato ocorrido no sistema.
A estrutura de um evento típico inclui:
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "PedidoCriado",
"timestamp": "2025-01-15T10:30:00Z",
"aggregateId": "pedido-12345",
"aggregateVersion": 1,
"data": {
"clienteId": "cli-789",
"itens": [
{"produto": "notebook", "quantidade": 1, "valor": 4500}
],
"total": 4500
},
"metadata": {
"userId": "usr-456",
"source": "web-app",
"correlationId": "corr-abc"
}
}
Para reconstruir o estado atual, percorremos todos os eventos do agregado e aplicamos cada um sequencialmente:
funcao reconstruirEstado(eventos):
estado = EstadoInicial()
para cada evento em eventos:
estado.aplicar(evento)
retornar estado
3. Arquitetura e componentes essenciais
O Event Store é o banco de dados especializado que armazena eventos. Diferente de bancos relacionais, ele otimiza para append (adição) e leitura sequencial. EventStoreDB é o exemplo mais conhecido, mas bancos como PostgreSQL também podem ser adaptados.
Agregados são objetos que garantem consistência dentro de um limite transacional. Eles recebem comandos, validam regras de negócio e emitem eventos:
agregado Pedido:
estado: Pendente, Confirmado, Enviado, Cancelado
itens: lista de itens
total: decimal
funcao processarComando(AdicionarItem comando):
se estado == Cancelado:
erro("Pedido cancelado não aceita itens")
evento = ItemAdicionado(comando.produto, comando.quantidade)
aplicarEvento(evento)
funcao aplicarEvento(ItemAdicionado evento):
itens.adicionar(evento.produto, evento.quantidade)
total += evento.valor
Projeções geram visões otimizadas para leitura. Elas escutam eventos e constroem modelos de dados específicos para consultas:
projecao ResumoPedido:
banco = TabelaRelacional()
funcao quando(ItemAdicionado evento):
banco.executar(
"UPDATE pedidos_resumo SET total = total + ? WHERE id = ?",
evento.valor, evento.aggregateId
)
4. Padrões de design e implementação
Comandos vs Eventos: comandos representam intenções e podem ser rejeitados. Eventos representam fatos consumados. A validação ocorre antes da persistência do evento:
funcao handler(ConfirmarPedido comando):
pedido = repositorio.carregar(comando.pedidoId)
se pedido.estado != "Pendente":
erro("Apenas pedidos pendentes podem ser confirmados")
evento = PedidoConfirmado(comando.pedidoId, timestamp())
repositorio.salvar(pedido.id, evento)
Versionamento de eventos: eventos evoluem. Estratégias incluem:
// Versão 1
evento ItemAdicionado:
produto string
quantidade int
valor decimal
// Versão 2 (adiciona desconto)
evento ItemAdicionado_v2:
produto string
quantidade int
valor decimal
desconto decimal
valorFinal decimal
Snapshotting: para agregados com milhares de eventos, armazenamos snapshots periódicos do estado:
funcao carregarAgregado(id):
snapshot = obterSnapshotMaisRecente(id)
eventos = obterEventosApos(id, snapshot.version)
estado = snapshot.estado
para cada evento em eventos:
estado.aplicar(evento)
retornar estado
5. Integração com CQRS e microsserviços
Event Sourcing e CQRS (Command Query Responsibility Segregation) formam uma combinação natural. A separação entre escrita (event store) e leitura (projeções) permite otimizar cada lado independentemente:
// Serviço de escrita - processa comandos e persiste eventos
servico EscritaPedidos:
funcao criarPedido(comando):
evento = PedidoCriado(...)
eventStore.append(evento)
eventBus.publicar(evento)
// Serviço de leitura - mantém projeções atualizadas
servico LeituraPedidos:
funcao quando(PedidoCriado evento):
projecaoResumo.inserir(evento)
projecaoItens.inserir(evento)
A publicação de eventos via event bus (Kafka, RabbitMQ) permite que múltiplos serviços reajam ao mesmo evento, mantendo consistência eventual.
6. Desafios práticos e armadilhas comuns
Complexidade operacional: gerenciar versões de eventos, migrações e snapshots exige ferramentas maduras. Sem disciplina, o event store vira um depósito de dados inconsistentes.
Erros comuns: eventos imutáveis mal projetados (que deveriam ser atualizados), dependências temporais (eventos que assumem estado anterior específico), e eventos com informações insuficientes para auditoria.
Custos de armazenamento: eventos nunca são deletados. Em escala, os custos de armazenamento e desempenho de reconstrução exigem snapshotting agressivo e arquivamento de eventos antigos.
7. Quando NÃO usar Event Sourcing
Event Sourcing não é bala de prata. Evite em:
- Sistemas com estado simples: um contador que só incrementa não precisa de eventos.
- Alta concorrência com consistência forte: sistemas de reservas de assentos em tempo real sofrem com latência de reconstrução.
- Times pequenos sem experiência: a complexidade adicional raramente compensa se não há necessidade real de auditoria ou histórico.
O trade-off fundamental: Event Sourcing adiciona complexidade significativa em troca de rastreabilidade completa e flexibilidade de projeções. Avalie se os benefícios justificam o custo.
8. Exemplo prático: sistema de pedidos
Implementação completa do fluxo comando → evento → projeção → consulta:
// Agregado Pedido
agregado Pedido:
id string
estado string = "Pendente"
itens = []
total = 0
funcao adicionarItem(produto, quantidade, valor):
se estado != "Pendente":
erro("Pedido não está pendente")
evento = ItemAdicionado(id, produto, quantidade, valor)
aplicar(evento)
retornar evento
funcao confirmar():
se estado != "Pendente":
erro("Pedido não está pendente")
se itens.vazio():
erro("Pedido sem itens")
evento = PedidoConfirmado(id, timestamp())
aplicar(evento)
retornar evento
funcao aplicar(evento):
se evento tipo ItemAdicionado:
itens.adicionar(evento)
total += evento.valor * evento.quantidade
se evento tipo PedidoConfirmado:
estado = "Confirmado"
// Projeção para consulta de resumo
projecao ResumoPedido:
tabela = "pedidos_resumo"
funcao quando(ItemAdicionado evento):
sql = "UPDATE pedidos_resumo SET total = total + ? WHERE id = ?"
banco.executar(sql, evento.valor * evento.quantidade, evento.aggregateId)
funcao quando(PedidoConfirmado evento):
sql = "UPDATE pedidos_resumo SET estado = 'Confirmado' WHERE id = ?"
banco.executar(sql, evento.aggregateId)
// Teste de comportamento
teste "Pedido com itens é confirmado com sucesso":
pedido = Pedido("pedido-1")
pedido.aplicar(ItemAdicionado("pedido-1", "notebook", 1, 4500))
pedido.aplicar(ItemAdicionado("pedido-1", "mouse", 2, 150))
evento = pedido.confirmar()
assert evento tipo PedidoConfirmado
assert pedido.total == 4800
assert pedido.estado == "Confirmado"
teste "Pedido sem itens não pode ser confirmado":
pedido = Pedido("pedido-2")
erro = capturarErro(() => pedido.confirmar())
assert erro.mensagem == "Pedido sem itens"
A beleza desse modelo é que cada transição de estado é registrada como um evento imutável. Para auditar o histórico, basta ler todos os eventos do agregado:
consulta HistoricoPedido(id):
eventos = eventStore.lerEventos(id)
para cada evento em eventos:
imprimir(evento.timestamp, evento.eventType, evento.data)
Referências
- Event Sourcing pattern - Microsoft Azure Architecture Center — Documentação oficial da Microsoft com definição, diagramas e considerações de implementação
- Event Sourcing - Martin Fowler — Artigo seminal de Martin Fowler explicando os fundamentos e motivações do padrão
- EventStoreDB Documentation — Documentação oficial do banco de dados especializado em Event Sourcing, com tutoriais e exemplos práticos
- CQRS and Event Sourcing - Greg Young — Documento original de Greg Young que estabeleceu as bases do CQRS combinado com Event Sourcing
- Event Sourcing with Kafka - Confluent — Tutorial prático de como implementar Event Sourcing usando Apache Kafka como event store
- Versioning in an Event Sourced System - Greg Young — Livro online gratuito sobre estratégias de versionamento de eventos em sistemas baseados em Event Sourcing
- Event Sourcing: What it is and why it's awesome - EventStore Blog — Introdução acessível ao conceito com exemplos do mundo real