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.
"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.
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
// 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).
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.
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.
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.
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
-
Implemente um ledger bancário simples. Crie um aggregate
BankAccountcom eventosAccountOpened,MoneyDeposited,MoneyWithdrawneAccountFrozen. Use PostgreSQL como event store com uma tabela destream_id,version,typeepayload 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. - 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.
-
Simule evolução de esquema. Adicione um campo opcional
categoryao eventoMoneyDeposited. Implemente upcasting que injetacategory: "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
- artigo CQRS Documents — Greg Young (2010).
- livro Implementing Domain-Driven Design — Vaughn Vernon (2013).
- artigo Event Sourcing — Martin Fowler (2005, revisado 2021).
- artigo The Log: What every software engineer should know about real-time data's unifying abstraction — Jay Kreps (2013).
- livro Designing Data-Intensive Applications — Martin Kleppmann (2017).
- livro Designing Event-Driven Systems — Ben Stopford (2018).
- artigo Versioning in an Event Sourced System — Greg Young (2016).
- docs EventStoreDB Documentation — EventStore Ltd.
- docs Marten — .NET Transactional Document DB and Event Store on PostgreSQL.
- artigo What do you mean by "Event-Driven"? — Martin Fowler (2017).
- vídeo CQRS and Event Sourcing — Greg Young (NDC 2014).
- livro Building Microservices — Sam Newman (2ª ed., 2021).