MÓDULO 07 · CONCEITO 13 DE 13

Event Sourcing

Estado como sequência imutável de eventos — a história é a fonte de verdade

Tempo de leitura ~22 min Pré-requisito 12 · CQRS e read-models Próximo Módulo 08 · Disponibilidade & Resiliência

Em 2010, Greg Young publicou um documento que ficou conhecido como "CQRS Documents" e incluiu uma ideia que parecia óbvia depois de lida, mas radical antes: em vez de persistir o estado atual de um aggregate, persistir os eventos que transformaram esse aggregate ao longo do tempo. O estado atual passa a ser uma derivação — um fold sobre a sequência de eventos, calculado quando necessário. Young chamou esse padrão de Event Sourcing.

A ideia tem precedente milenar em contabilidade. Um saldo bancário não é um número armazenado em algum lugar e atualizado a cada transação. É a soma de todas as transações, calculada sobre um ledger imutável de débitos e créditos. O ledger nunca muda — transações são adicionadas, nunca editadas ou removidas. Se o saldo está errado, você adiciona uma transação corretiva, não corrige a antiga. Contadores sabem há séculos que apagar o passado destrói a rastreabilidade. Event Sourcing traz essa disciplina para o design de software.

O CQRS do conceito anterior separa o modelo de escrita do modelo de leitura. Event Sourcing especifica como o lado de escrita funciona: em vez de um UPDATE accounts SET balance = 1500 WHERE id = 42, você registra MoneyDeposited { accountId: 42, amount: 500, at: "2024-03-15T10:30:00Z" }. O estado atual é o resultado de aplicar todos os eventos em ordem. Event Sourcing e CQRS são complementares — frequentemente usados juntos porque se reforçam — mas independentes: você pode ter CQRS sem Event Sourcing (read models derivados de tabelas convencionais) e Event Sourcing sem CQRS (um único modelo que reconstitui seu estado por replay).

Este conceito cobre o modelo mental, a mecânica de um event store, projeções, versionamento de eventos, os trade-offs reais de adotar o padrão, e quando Event Sourcing é a resposta errada — o que acontece com mais frequência do que os entusiastas admitem.

O modelo mental: estado como derivação

Em sistemas convencionais, o banco de dados armazena o estado atual. Um UPDATE substitui o estado anterior. Você sabe o que está armazenado hoje, mas não como chegou lá. Se um campo muda de valor, o valor antigo some. Auditoria exige infraestrutura adicional — triggers, tabelas de histórico, logs de aplicação construídos sobre o sistema, porque o sistema em si não guarda a história.

Event Sourcing inverte a relação. O event store guarda a história — uma sequência ordenada e imutável de eventos. O estado atual é calculado aplicando esses eventos em ordem, como uma função de redução. Na linguagem funcional: state = events.reduce(apply, initialState). Em contabilidade: saldo = transações.reduce(soma, 0). O estado atual nunca é persistido diretamente — é sempre derivado, sempre recalculável, sempre auditável sem esforço adicional porque a auditoria é a consequência natural do modelo.

princípio orientador

"The current state is a left fold over the event history." — Greg Young (2010). Essa frase concentra o modelo mental inteiro. O estado não é o que está no banco agora — é a consequência matemática de tudo que aconteceu antes.

A analogia com o Git é precisa e pedagogicamente valiosa. Cada commit é um evento — imutável, ordenado, com autor e timestamp. O working tree é o estado atual — derivado dos commits. Você pode reconstituir qualquer estado passado com git checkout <sha>. Você pode criar branches (projeções alternativas sobre o mesmo histórico). Pode inspecionar qualquer mudança com git log e git diff. Event Sourcing é Git para o estado de domínio da sua aplicação — com a mesma garantia de que o histórico é a fonte de verdade, não uma representação derivada dele.

A estrutura de um event store

Um event store é fundamentalmente um log append-only particionado por stream. Cada aggregate — na linguagem DDD, a unidade de consistência do domínio — tem seu próprio stream. Um stream de conta bancária com id 42 contém todos os eventos daquela conta específica, em ordem, com versão monotônica crescente. Ninguém escreve no meio do stream; ninguém apaga eventos existentes. A única operação de escrita é append.

A estrutura mínima de um evento tem três partes: o tipo (o que aconteceu — OrderPlaced, PaymentProcessed, ItemShipped), o payload (os dados do evento — o quanto, o quê, para quem, sob quais condições), e os metadados (quando, por quem, via qual comando, com qual correlation id, causation id). Tipos de evento devem nomear fatos de domínio no passado — nunca comandos. OrderPlaced, não PlaceOrder. MoneyWithdrawn, não WithdrawMoney. Eventos descrevem o que aconteceu; comandos expressam intenção futura. Confundir os dois é um erro de modelagem com consequências de longo prazo.

Controle de concorrência é implementado via número de versão do stream. Quando você carrega um aggregate, registra a versão atual. Quando persiste novos eventos, especifica a versão esperada. Se outro processo escreveu no mesmo stream enquanto isso, a versão não bate e a escrita falha com conflito de concorrência — o que desencadeia retry com a lógica de domínio aplicada sobre o estado atualizado. Esse mecanismo é semanticamente equivalente ao optimistic locking de ORMs, mas expresso em termos de sequência de eventos em vez de timestamp ou rowversion.

Snapshots para performance

O custo de reconstituir o estado de um aggregate é proporcional ao número de eventos em seu stream. Uma conta bancária com dez anos de transações pode ter dezenas de milhares de eventos. Reconstituir seu saldo a cada operação relendo o stream completo é inaceitável em sistemas de alta frequência. Snapshots resolvem isso: periodicamente — a cada N eventos, conforme o perfil de acesso — você persiste o estado atual calculado como um snapshot. Para carregar o aggregate, lê o snapshot mais recente e aplica apenas os eventos posteriores a ele.

Snapshots são sempre opcionais e sempre deriváveis. Se você apagar todos os snapshots, o sistema continua funcionando — apenas mais lento para aggregates com histórico longo. A fonte de verdade permanece o event stream. Snapshots são uma otimização de leitura, não parte do modelo de dados. Isso os distingue radicalmente de um banco relacional convencional, onde o estado armazenado é a fonte de verdade e não pode ser descartado sem perda de dados.

armadilha em produção

Não implemente snapshots antes de ter problema de performance comprovado com profiling. A maioria dos aggregates tem histórico curto na prática — centenas de eventos, não dezenas de milhares. Snapshots prematuros adicionam complexidade (invalidação, migração de formato, sincronização) sem benefício mensurável e dificultam o debugging de problemas de reconstituição.

Projeções: read models derivados de eventos

Em um sistema com Event Sourcing, os read models são projeções sobre os event streams. Uma projeção é uma função que processa eventos em ordem e mantém um estado derivado otimizado para leitura — estrutura diferente do aggregate, desnormalizada, indexada para consulta. A mesma sequência de eventos pode produzir múltiplas projeções independentes: um read model para a tela de detalhe do pedido, outro para o relatório de vendas por região, outro para o dashboard de estoque em tempo real. Cada projeção é construída exatamente para o padrão de consulta que serve, sem compromisso com as outras.

Projeções podem ser síncronas (calculadas no momento da escrita, na mesma transação) ou assíncronas (processadas por um worker que consome o event stream em background). Projeções assíncronas introduzem consistência eventual — o read model pode estar levemente atrás do event store — mas escalam independentemente da escrita e podem ser reconstruídas do zero a qualquer momento replaying o event stream desde o início. Essa capacidade de replay é uma das propriedades mais valiosas de Event Sourcing: se uma projeção for corrompida, ou se você precisar de um novo read model que não existia antes, basta processar o histórico inteiro novamente.

O processo de replay exige que as projeções sejam idempotentes e que os event handlers não tenham side effects externos durante a reconstrução. Re-enviar e-mails, re-cobrar cartões, re-notificar serviços externos durante um replay de projeção é um desastre de produção. Separar consequências internas (atualizar read models) de efeitos externos (comunicação com outros sistemas) é uma disciplina de design que Event Sourcing torna explícita e necessária — uma das formas como o padrão melhora a arquitetura além do caso de uso de auditoria.

Implementação nas três linguagens

C# — Marten sobre PostgreSQL
// Marten v7+ usa PostgreSQL como event store nativo
// via JSONB + sequência monotônica por stream.

public record MoneyDeposited(
    Guid AccountId, decimal Amount, DateTimeOffset At);

public record MoneyWithdrawn(
    Guid AccountId, decimal Amount, DateTimeOffset At);

public class BankAccount
{
    public Guid Id { get; private set; }
    public decimal Balance { get; private set; }
    public int Version { get; private set; }

    public void Apply(MoneyDeposited e)
    {
        Id = e.AccountId;
        Balance += e.Amount;
        Version++;
    }
    public void Apply(MoneyWithdrawn e)
    {
        Balance -= e.Amount;
        Version++;
    }
}

// Append — persiste evento no stream do aggregate
await using var session = store.LightweightSession();
session.Events.Append(accountId,
    new MoneyDeposited(accountId, 500m, DateTimeOffset.UtcNow));
await session.SaveChangesAsync();

// Reconstituição via replay automático
var account = await session
    .Events
    .AggregateStreamAsync<BankAccount>(accountId);

Console.WriteLine(account.Balance); // 500

Marten elimina a necessidade de um event store separado — PostgreSQL passa a ser o event store via JSONB com versionamento por stream. AggregateStreamAsync faz o replay automaticamente chamando os métodos Apply correspondentes ao tipo do evento. Snapshots são configuráveis via InlineSnapshot (síncrono) ou AsyncDaemon (assíncrono com projections em background).

Python — implementação com PostgreSQL e asyncpg
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
import asyncpg, json

@dataclass
class MoneyDeposited:
    account_id: str
    amount: float
    at: str

@dataclass
class BankAccount:
    id: str = ""
    balance: float = 0.0
    version: int = 0

    def apply(self, event) -> None:
        match event:
            case MoneyDeposited():
                self.id = event.account_id
                self.balance += event.amount
                self.version += 1

async def append_event(conn, stream_id: str, event) -> None:
    await conn.execute("""
        INSERT INTO events (stream_id, version, type, payload)
        VALUES ($1,
            (SELECT COALESCE(MAX(version), 0) + 1
             FROM events WHERE stream_id = $1),
            $2, $3)
    """, stream_id, type(event).__name__,
        json.dumps(asdict(event)))

EVENT_TYPES = {"MoneyDeposited": MoneyDeposited}

async def load_account(conn, account_id: str) -> BankAccount:
    rows = await conn.fetch(
        "SELECT type, payload FROM events "
        "WHERE stream_id = $1 ORDER BY version",
        account_id)
    account = BankAccount()
    for row in rows:
        cls = EVENT_TYPES[row["type"]]
        event = cls(**json.loads(row["payload"]))
        account.apply(event)
    return account

Python não tem uma biblioteca de event sourcing com adoção equivalente ao Marten no ecossistema .NET. As implementações de produção usam PostgreSQL com asyncpg, EventStoreDB com o client oficial esdbclient, ou a biblioteca eventsourcing dedicada ao pattern. O match do Python 3.10+ torna os handlers expressivos e exaustivos com type narrowing implícito.

Go — EventStoreDB com client oficial
package account

import (
    "context"
    "encoding/json"
    esdb "github.com/EventStore/EventStore-Client-Go/v4/esdb"
)

type MoneyDeposited struct {
    AccountID string  `json:"account_id"`
    Amount    float64 `json:"amount"`
}

func AppendDeposit(ctx context.Context, client *esdb.Client,
    streamID string, e MoneyDeposited) error {

    payload, _ := json.Marshal(e)
    _, err := client.AppendToStream(ctx, streamID,
        esdb.AppendToStreamOptions{
            ExpectedRevision: esdb.Any{},
        },
        esdb.EventData{
            EventType:   "MoneyDeposited",
            ContentType: esdb.ContentTypeJson,
            Data:        payload,
        })
    return err
}

func LoadBalance(ctx context.Context, client *esdb.Client,
    streamID string) (float64, error) {

    stream, err := client.ReadStream(ctx, streamID,
        esdb.ReadStreamOptions{}, 4096)
    if err != nil {
        return 0, err
    }
    defer stream.Close()

    var balance float64
    for {
        ev, err := stream.Recv()
        if err != nil {
            break
        }
        var e MoneyDeposited
        json.Unmarshal(ev.Event.Data, &e)
        balance += e.Amount
    }
    return balance, nil
}

EventStoreDB é o banco criado especificamente para Event Sourcing — por Greg Young e equipe. Suporta streams, subscriptions catch-up e persistent, e projections em JavaScript rodando no servidor. O client Go está em github.com/EventStore/EventStore-Client-Go. Para projetos menores, PostgreSQL com pgx e uma tabela de eventos com stream_id, version e payload JSONB é suficiente e mais simples de operar.

Versionamento de eventos: o problema mais difícil

Event streams são imutáveis e — em princípio — eternos. Mas o código que processa esses eventos evolui. A estrutura de OrderPlaced de três anos atrás pode não ter o campo promoCode que você adicionou no ano seguinte. Versionamento de eventos é o problema mais subestimado de Event Sourcing, frequentemente ignorado durante a adoção inicial e dolorosamente descoberto quando o sistema está em produção.

Existem quatro estratégias principais, em ordem crescente de complexidade. Weak schema (esquema fraco) torna todos os campos opcionais e usa valores padrão para campos ausentes — funciona bem para adições, mal para remoções e renomeações. Upcasting transforma eventos de versões antigas para o formato atual no momento da leitura — o event store guarda o evento original intacto, o código de leitura o converte; é a estratégia mais recomendada para a maioria dos casos. Event versioning explícito usa tipos como OrderPlaced.v2 — mais explícito, mais verboso, requer manter múltiplos handlers por tipo. Event migration re-escreve o event stream com novos eventos — perigoso, raramente necessário, requer procedimento cuidadoso e janela de manutenção. O livro Versioning in an Event Sourced System de Greg Young (2016) é a referência mais completa sobre o tema.

armadilha clássica

Nunca design eventos capturando estado — design eventos capturando intenção. BalanceUpdated { newBalance: 1500 } é estado: diz o que ficou, não o que aconteceu. MoneyDeposited { amount: 500 } é intenção: diz o que foi feito e por quê o saldo mudou. O primeiro perde a semântica de domínio e torna replay e auditoria inúteis. O segundo preserva o porquê e permite recalcular qualquer derivação com significado.

Quando usar Event Sourcing — e quando não usar

Event Sourcing resolve problemas específicos muito bem e cria complexidade desnecessária em todo o resto. Os contextos onde o padrão entrega valor real são identificáveis: domínios onde auditoria completa é requisito de negócio ou regulatório (financeiro, saúde, jurídico — "quem fez o quê e quando" não é opcional); sistemas onde reconstituir estado passado é valioso (reservas, inventário, workflow de aprovação com histórico de estados); domínios com regras de negócio que dependem do histórico de transições, não apenas do estado atual; sistemas onde múltiplas projeções do mesmo dado são necessárias para contextos diferentes sem duplicar a lógica de negócio.

Os casos onde Event Sourcing é a resposta errada são mais comuns: entidades simples com poucos atributos e sem histórico relevante (configurações de usuário, metadados de produto, conteúdo editorial); sistemas onde a equipe não tem familiaridade com o padrão e o domínio não justifica a curva de aprendizado; aplicações essencialmente CRUD onde o estado atual é a única coisa que importa e auditoria pode ser resolvida com uma tabela de log convencional.

heurística do sênior

A pergunta correta não é "como persistimos eventos?" mas "o histórico de eventos é informação de negócio?" Se a resposta for não, Event Sourcing adiciona complexidade sem valor. Se a resposta for sim, a pergunta seguinte é: uma tabela de auditoria convencional (created_at, updated_at, diff em JSON) não resolve o mesmo problema com menos esforço? Event Sourcing justifica-se quando o evento em si — com sua semântica de domínio — é a informação primária, e o estado atual é o derivado.

Event Sourcing e o ecossistema de dados

Um event store bem projetado é uma fonte natural para pipelines de dados. Os mesmos eventos que reconstituem aggregates podem alimentar data lakes, sistemas de analytics em tempo real, e integrações com outros Bounded Contexts via event-driven architecture. Martin Kleppmann, em Designing Data-Intensive Applications (2017), conecta o modelo de log imutável de Event Sourcing com o conceito de change data capture (CDC) e streaming platforms como Kafka: o event store é o canal canônico entre o sistema que escreve e todos os sistemas que precisam reagir ao que acontece.

Essa propriedade é especialmente valiosa em arquiteturas de microsserviços onde Bounded Contexts precisam se comunicar sem acoplamento direto. Em vez de chamadas síncronas entre serviços, os eventos publicados pelo aggregate são consumidos por outros contextos para construir suas próprias projeções. O event store se torna a infraestrutura de integração — mais do que um mecanismo de persistência local.

Como praticar

  1. Implemente um ledger bancário simples. Crie um aggregate BankAccount com eventos AccountOpened, MoneyDeposited, MoneyWithdrawn e AccountFrozen. Use PostgreSQL como event store com uma tabela de stream_id, version, type e payload JSONB. Implemente reconstituição via replay e uma projeção de saldo para leitura. Compare com uma implementação CRUD equivalente e mensure a diferença de complexidade — esse exercício calibra quando o padrão vale a pena.
  2. Adicione uma segunda projeção ao mesmo event stream. Sobre os mesmos eventos bancários, construa um read model de extrato formatado para exibição: data, descrição, saldo anterior, saldo posterior por transação. Observe como a mesma sequência de eventos produz dois read models com estrutura completamente diferente sem modificar o event store ou o aggregate. Implemente replay da projeção do zero.
  3. Simule evolução de esquema. Adicione um campo opcional category ao evento MoneyDeposited. Implemente upcasting que injeta category: "uncategorized" em eventos antigos que não têm o campo. Replaye o stream inteiro com a nova versão do handler e verifique que o aggregate reconstitui corretamente tanto eventos antigos quanto novos.

Referências para aprofundar

  1. artigo CQRS Documents — Greg Young (2010). cqrs.files.wordpress.com — O documento original onde Young sistematiza Event Sourcing junto com CQRS. Leitura obrigatória antes de qualquer implementação.
  2. livro Implementing Domain-Driven Design — Vaughn Vernon (2013). Addison-Wesley. Capítulos 8 e 9 cobrem Event Sourcing com DDD tático em profundidade. A implementação mais completa e madura do padrão disponível em livro.
  3. artigo Event Sourcing — Martin Fowler (2005, revisado 2021). martinfowler.com/eaaDev/EventSourcing.html — A definição canônica de Fowler. Mais curta e acessível que o documento de Young; bom ponto de entrada.
  4. artigo The Log: What every software engineer should know about real-time data's unifying abstraction — Jay Kreps (2013). engineering.linkedin.com — Conecta a ideia de log imutável — fundação do ES — com streaming platforms e bancos de dados distribuídos. Influenciou a criação do Kafka.
  5. livro Designing Data-Intensive Applications — Martin Kleppmann (2017). O'Reilly. Capítulo 11 conecta Event Sourcing com Change Data Capture, stream processing e integração entre sistemas. A perspectiva mais abrangente sobre logs imutáveis.
  6. livro Designing Event-Driven Systems — Ben Stopford (2018). O'Reilly (gratuito online). Conecta Event Sourcing com Kafka como event store e streaming backbone. Perspectiva de sistemas distribuídos em escala.
  7. artigo Versioning in an Event Sourced System — Greg Young (2016). leanpub.com — O tratado mais completo sobre versionamento de eventos. Cobre weak schema, upcasting, copy-transform, e quando cada estratégia se aplica.
  8. docs EventStoreDB Documentation — EventStore Ltd. developers.eventstore.com — Documentação do banco criado especificamente para Event Sourcing: streams, subscriptions, projections no servidor.
  9. docs Marten — .NET Transactional Document DB and Event Store on PostgreSQL. martendb.io — Transforma PostgreSQL em event store para .NET. Inclui snapshots, AsyncDaemon para projections, e integração nativa com CQRS.
  10. artigo What do you mean by "Event-Driven"? — Martin Fowler (2017). martinfowler.com — Distingue Event Notification, Event-Carried State Transfer, Event Sourcing e CQRS. Clarifica confusões terminológicas que persistem no mercado.
  11. vídeo CQRS and Event Sourcing — Greg Young (NDC 2014). YouTube. Young explica o padrão com exemplos de código e casos de produção reais. Uma das palestras mais assistidas sobre o tema.
  12. livro Building Microservices — Sam Newman (2ª ed., 2021). O'Reilly. Capítulo sobre Event Sourcing e sagas em contexto de microsserviços. Perspectiva prática de quem viu implementações boas e ruins em produção.