O que Todo Dev Deve Saber sobre Event Sourcing Já leu

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

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.

Referências


Artigos relacionados