O que é Event Sourcing
Event Sourcing é um padrão arquitetural onde você armazena todas as mudanças de estado de uma aplicação como uma sequência imutável de eventos. Em vez de persistir apenas o estado atual (como faz um banco de dados tradicional), você registra cada ação que ocorreu. Isso significa que o histórico completo está sempre disponível, e você pode reconstruir qualquer estado anterior simplesmente reproduzindo os eventos.
A principal diferença em relação ao paradigma CRUD tradicional é que aqui não você atualiza um registro — você registra que algo aconteceu. Essa abordagem traz benefícios como auditoria natural, debugging facilitado, e possibilidade de projeções múltiplas do mesmo dado. No entanto, adiciona complexidade operacional que deve ser bem compreendida antes de aplicar em produção.
Por que usar Event Sourcing?
Auditoria e Conformidade: Cada mudança fica registrada permanentemente com timestamp. Para setores como financeiro e healthcare, isso é ouro puro. Debugging e Troubleshooting: Você pode reproduzir exatamente o que aconteceu em um dado momento. CQRS (Command Query Responsibility Segregation): Separar escrita (comandos) de leitura (queries) fica naturalmente elegante. Event-Driven Architecture: Permite comunicação assincrônica entre microsserviços através de eventos.
Arquitetura e Componentes Principais
Um sistema com Event Sourcing típico possui: o Event Store (banco imutável que armazena eventos), o Aggregate (entidade que processa comandos e emite eventos), Projections (visões processadas dos eventos para consulta rápida), e Event Handlers (reagem aos eventos para efeitos colaterais).
O fluxo é simples: usuário executa uma ação → comando é validado → aggregate processa e emite evento → evento é persistido → projeções são atualizadas → subscribers reagem. A garantia de imutabilidade do event store é crítica: você nunca deleta ou altera eventos, apenas adiciona novos.
Event Store
É o coração do sistema. Você precisa garantir que cada evento seja persistido de forma ordenada e recuperável. Existem soluções especializadas como EventStoreDB e Axon Framework, mas você também pode implementar com PostgreSQL ou MongoDB.
// Exemplo simples de Event Store em Node.js com PostgreSQL
const { Pool } = require('pg');
class EventStore {
constructor() {
this.pool = new Pool({
connectionString: 'postgresql://user:password@localhost/eventstore'
});
}
async appendEvent(aggregateId, eventType, eventData, metadata = {}) {
const query = `
INSERT INTO events (aggregate_id, event_type, event_data, metadata, created_at)
VALUES ($1, $2, $3, $4, NOW())
RETURNING *
`;
const result = await this.pool.query(query, [
aggregateId,
eventType,
JSON.stringify(eventData),
JSON.stringify(metadata)
]);
return result.rows[0];
}
async getEventsByAggregateId(aggregateId) {
const query = 'SELECT * FROM events WHERE aggregate_id = $1 ORDER BY created_at ASC';
const result = await this.pool.query(query, [aggregateId]);
return result.rows;
}
}
module.exports = EventStore;
Aggregates e Projeções
Um Aggregate é uma entidade que encapsula lógica de negócio. Ele recebe comandos, aplica regras e emite eventos. Uma Projeção é uma visão construída a partir de eventos, otimizada para leitura rápida.
// Aggregate de Conta Bancária
class BankAccountAggregate {
constructor(accountId) {
this.accountId = accountId;
this.balance = 0;
this.status = 'active';
this.changes = [];
}
deposit(amount) {
if (amount <= 0) throw new Error('Invalid amount');
this.recordEvent('MoneyDeposited', { amount, timestamp: new Date() });
}
withdraw(amount) {
if (amount > this.balance) throw new Error('Insufficient funds');
this.recordEvent('MoneyWithdrawn', { amount, timestamp: new Date() });
}
recordEvent(eventType, eventData) {
this.changes.push({ eventType, eventData });
this.applyEvent(eventType, eventData);
}
applyEvent(eventType, eventData) {
if (eventType === 'MoneyDeposited') {
this.balance += eventData.amount;
} else if (eventType === 'MoneyWithdrawn') {
this.balance -= eventData.amount;
}
}
loadFromHistory(events) {
events.forEach(event => {
this.applyEvent(event.event_type, JSON.parse(event.event_data));
});
}
}
// Projeção para leitura rápida
class AccountBalanceProjection {
constructor() {
this.accounts = new Map();
}
handleMoneyDeposited(accountId, eventData) {
const account = this.accounts.get(accountId) || { balance: 0 };
account.balance += eventData.amount;
account.lastUpdated = eventData.timestamp;
this.accounts.set(accountId, account);
}
handleMoneyWithdrawn(accountId, eventData) {
const account = this.accounts.get(accountId) || { balance: 0 };
account.balance -= eventData.amount;
account.lastUpdated = eventData.timestamp;
this.accounts.set(accountId, account);
}
getBalance(accountId) {
const account = this.accounts.get(accountId);
return account ? account.balance : 0;
}
}
module.exports = { BankAccountAggregate, AccountBalanceProjection };
Desafios e Boas Práticas
Event Sourcing não é uma solução universal. O principal desafio é a eventual consistency: quando você escreve um evento, as projeções podem levar tempo para atualizar. Isso exige que sua aplicação aceite inconsistência temporária. Além disso, o crescimento do event store é inevitável — com milhões de eventos, reconstruir um aggregate do zero fica lento. A solução é usar snapshots: periodicamente salve o estado calculado para não precisar reprocessar todos os eventos.
Outro ponto crítico: versionamento de eventos. Requisitos mudam, e você não pode simplesmente deletar eventos antigos. Implemente versioning desde o início, com lógica de migração para lidar com esquemas desatualizados.
// Boas práticas: Snapshot e Versionamento
class SnapshotStore {
constructor() {
this.snapshots = new Map();
}
saveSnapshot(aggregateId, version, state) {
this.snapshots.set(aggregateId, { version, state, timestamp: new Date() });
}
getSnapshot(aggregateId) {
return this.snapshots.get(aggregateId);
}
}
// Handler com tratamento de versões
function handleEvent(eventType, eventVersion, eventData) {
if (eventType === 'MoneyDeposited') {
if (eventVersion === 1) {
// Lógica legada
return { amount: eventData.amount };
} else if (eventVersion === 2) {
// Nova lógica com taxa
return { amount: eventData.amount, fee: eventData.fee || 0 };
}
}
}
Quando Usar Event Sourcing
Aplique Event Sourcing em domínios onde auditoria é crítica (financeiro, saúde, compliance), histórico é valor (análise temporal, machine learning), ou comunicação entre sistemas é complexa (microsserviços). Evite em aplicações CRUD simples, sistemas com latência crítica (real-time gaming), ou quando o overhead não se justifica.
Um e-commerce, por exemplo, se beneficia: rastrear pedidos, devoluções, reembolsos. Uma aplicação de to-do list? Provavelmente não.
Conclusão
Event Sourcing é poderoso, mas não é bala de prata. O grande aprendizado é entender que estado é derivado de eventos, não o inverso. Isso muda fundamentalmente como você pensa sobre persistência. Segundo, reconheça que eventual consistency é uma troca aceitável por auditoria, escalabilidade e simplicidade operacional. Terceiro, comece simples: implemente event store básico antes de adicionar snapshots, projeções complexas ou CQRS completo.