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