Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Event Sourcing no Node.js

Atualizado em: 26 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

O Event Sourcing no Node.js armazena mudanças de estado como uma sequência de eventos imutáveis. Em vez de manter apenas o resultado atual de um pedido, conta ou assinatura, o sistema registra fatos como OrderCreated, ItemAdded, PaymentAuthorized e OrderCancelled.

O estado atual é reconstruído aplicando os eventos em ordem. Isso oferece histórico completo, auditoria e capacidade de criar novas projeções. Porém, exige versionamento de eventos, controle de concorrência, snapshots, idempotência e ferramentas para evolução e operação.

Neste guia, você aprenderá a modelar streams, persistir eventos no PostgreSQL, reconstruir agregados, usar versões, snapshots, projeções, outbox, upcasting, testes e observabilidade.

O que é Event Sourcing?

Event Sourcing usa eventos como fonte de verdade. A referência Event Sourcing de Martin Fowler apresenta o padrão. A documentação Event Sourcing Pattern da Microsoft explica benefícios e desafios.

Para separar commands e queries, consulte CQRS no Node.js. Para publicação confiável, veja Outbox Pattern no Node.js.

Estado atual versus histórico

Em um modelo tradicional:

orders
id | status | total_cents | updated_at

No Event Sourcing:

order_events
stream_id | version | event_type | payload | occurred_at

O estado é derivado do histórico.

Evento é um fato

Use nomes no passado:

  • OrderCreated;
  • ProductAddedToOrder;
  • PaymentAuthorized;
  • OrderShipped.

Um evento não deve ser alterado depois de persistido.

Schema da tabela

CREATE TABLE event_store (
  global_position BIGSERIAL PRIMARY KEY,
  event_id UUID NOT NULL UNIQUE,
  stream_id TEXT NOT NULL,
  stream_type TEXT NOT NULL,
  stream_version INTEGER NOT NULL,
  event_type TEXT NOT NULL,
  event_version INTEGER NOT NULL,
  payload JSONB NOT NULL,
  metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
  occurred_at TIMESTAMPTZ NOT NULL,
  recorded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (stream_id, stream_version)
);

CREATE INDEX idx_event_store_stream
ON event_store (stream_id, stream_version);

A constraint por stream impede duas gravações na mesma versão.

Stream

Um stream representa o histórico de um agregado, por exemplo:

order-842

Todos os eventos desse pedido compartilham o mesmo stream ID.

Envelope do evento

{
  "eventId": "...",
  "streamId": "order-842",
  "streamVersion": 3,
  "eventType": "PaymentAuthorized",
  "eventVersion": 1,
  "occurredAt": "2026-08-26T14:00:00.000Z",
  "data": {
    "authorizationId": "auth-91",
    "amountCents": 15990
  },
  "metadata": {
    "correlationId": "...",
    "causationId": "..."
  }
}

Agregado

O agregado aplica eventos para construir estado e valida commands:

class Order {
  constructor() {
    this.id = null;
    this.status = 'empty';
    this.items = [];
    this.version = 0;
    this.pendingEvents = [];
  }

  apply(event) {
    switch (event.type) {
      case 'OrderCreated':
        this.id = event.data.orderId;
        this.status = 'created';
        break;
      case 'ProductAddedToOrder':
        this.items.push(event.data);
        break;
      case 'OrderCancelled':
        this.status = 'cancelled';
        break;
    }

    this.version += 1;
  }
}

Decisão de negócio

cancel(reason) {
  if (this.status === 'shipped') {
    throw new Error('Pedido enviado não pode ser cancelado');
  }

  if (this.status === 'cancelled') {
    return;
  }

  this.raise({
    type: 'OrderCancelled',
    data: { reason }
  });
}

O command produz um evento; o evento altera o estado.

raise()

raise(event) {
  this.apply(event);
  this.pendingEvents.push(event);
}

Eventos novos são aplicados e guardados para persistência.

Reidratação

function rehydrateOrder(events) {
  const order = new Order();

  for (const event of events) {
    order.apply(event);
  }

  order.pendingEvents = [];
  return order;
}

Carregando o stream

SELECT event_type,
       event_version,
       payload,
       metadata,
       stream_version,
       occurred_at
FROM event_store
WHERE stream_id = $1
ORDER BY stream_version;

Persistindo eventos

async function appendEvents(client, streamId, expectedVersion, events) {
  let version = expectedVersion;

  for (const event of events) {
    version += 1;

    await client.query(`
      INSERT INTO event_store (
        event_id,
        stream_id,
        stream_type,
        stream_version,
        event_type,
        event_version,
        payload,
        metadata,
        occurred_at
      ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
    `, [
      event.id,
      streamId,
      'order',
      version,
      event.type,
      event.version || 1,
      event.data,
      event.metadata || {},
      event.occurredAt
    ]);
  }
}

Concorrência otimista

Duas requisições podem carregar versão 3 e tentar gravar versão 4. A constraint permite apenas uma.

Tratando conflito

Converta a violação de unique em erro de concorrência e peça ao command para recarregar ou retornar conflito.

Consulte Lock Otimista no Node.js.

Transação

Todos os eventos de um command devem ser gravados na mesma transação. Se também houver outbox, grave junto.

Eventos pendentes

Depois do commit, limpe pendingEvents. Se o commit falha, o agregado não deve ser reutilizado como se tivesse persistido.

Global position

Uma posição global crescente ajuda projeções a consumir todos os eventos em ordem de gravação.

Checkpoint da projeção

CREATE TABLE projection_checkpoints (
  projection_name TEXT PRIMARY KEY,
  last_position BIGINT NOT NULL,
  updated_at TIMESTAMPTZ NOT NULL
);

A projeção continua da posição seguinte.

Projeção

async function project(event, client) {
  switch (event.eventType) {
    case 'OrderCreated':
      await client.query(`
        INSERT INTO order_view (
          order_id, status, total_cents, version
        ) VALUES ($1, 'created', 0, $2)
      `, [event.data.orderId, event.streamVersion]);
      break;

    case 'ProductAddedToOrder':
      await client.query(`
        UPDATE order_view
        SET total_cents = total_cents + $1,
            version = $2
        WHERE order_id = $3
          AND version = $4
      `, [
        event.data.totalCents,
        event.streamVersion,
        event.data.orderId,
        event.streamVersion - 1
      ]);
      break;
  }
}

Projeção idempotente

Registre event ID processado ou use versão condicional para que redelivery não duplique resultados.

Rebuild

Uma projeção pode ser recriada desde o primeiro evento. Faça em nova tabela e troque depois de validar.

Blue-green de projeção

  1. Criar order_view_v2.
  2. Reprocessar todo o histórico.
  3. Acompanhar novos eventos.
  4. Comparar resultados.
  5. Trocar queries.
  6. Remover a versão anterior.

Snapshots

Streams longos tornam a reidratação lenta. Um snapshot guarda estado em uma versão:

CREATE TABLE snapshots (
  stream_id TEXT NOT NULL,
  stream_version INTEGER NOT NULL,
  state JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL,
  PRIMARY KEY (stream_id, stream_version)
);

Carregando snapshot

Leia o snapshot mais recente e depois apenas eventos posteriores.

Snapshot não é fonte de verdade

Se o snapshot é perdido, o stream ainda deve reconstruir o estado.

Frequência de snapshot

Crie a cada quantidade de eventos ou quando o tempo de carregamento ultrapassar um limite. Não gere após todo evento.

Versionamento de eventos

Eventos antigos permanecem no store. Quando o schema muda, você precisa continuar entendendo versões anteriores.

Upcasting

function upcastProductAdded(event) {
  if (event.eventVersion === 1) {
    return {
      ...event,
      eventVersion: 2,
      data: {
        ...event.data,
        currency: 'BRL'
      }
    };
  }

  return event;
}

O upcaster transforma durante leitura sem alterar o evento original.

Não reescreva o histórico silenciosamente

Alterar eventos antigos quebra auditoria e projeções. Migrações de evento exigem plano, backup e registro explícito.

Eventos com dados pessoais

Imutabilidade entra em tensão com leis de privacidade. Evite dados desnecessários, use referências, criptografia por chave e políticas de anonimização planejadas.

Cripto-shredding

Dados sensíveis podem ser cifrados com chave por sujeito. Destruir a chave torna o conteúdo inacessível, mas o desenho precisa ser validado juridicamente.

Metadados

Inclua:

  • correlation ID;
  • causation ID;
  • actor ID;
  • tenant ID;
  • trace context;
  • versão da aplicação.

Não inclua tokens.

Auditoria

O histórico explica o que ocorreu, mas auditoria pode exigir informações adicionais de acesso e intenção.

Event Store versus broker

O event store persiste fatos de domínio. O broker distribui mensagens. Não trate Kafka automaticamente como event store sem avaliar retenção, consultas, concorrência e requisitos.

Publicação externa

Nem todo evento interno deve ser público. Converta eventos de domínio em integration events estáveis.

Outbox

Ao gravar eventos, uma projeção externa ou relay pode publicar integration events com outbox na mesma transação.

Idempotência

Commands repetidos devem produzir o mesmo resultado ou ser reconhecidos. Use command ID.

Veja Idempotência em APIs Node.js.

Event Sourcing e Saga

Uma saga pode registrar suas transições como eventos. Ainda precisa de compensações e mensagens confiáveis.

Event Sourcing e CQRS

É comum usar Event Sourcing no modelo de escrita e projeções no lado de leitura, mas adote apenas quando o histórico agrega valor.

Consultas temporais

Reproduza eventos até uma versão ou horário para investigar o estado passado.

Simulações

Você pode criar uma projeção experimental com o histórico, sem alterar a fonte de verdade.

Correção de erro

Em vez de editar um evento incorreto, grave um evento compensatório, como CustomerAddressCorrected.

Eventos inválidos historicamente

Se um bug gravou dados impossíveis, crie upcaster ou evento corretivo. Documente o incidente.

Multi-tenant

Inclua tenant na chave ou coluna e aplique autorização. Não permita ler streams por ID sem filtrar tenant.

Particionamento

Em alto volume, particione por tempo ou hash. A constraint de versão do stream precisa continuar efetiva.

Retenção

Event Sourcing normalmente mantém histórico indefinido, mas custo e compliance exigem política clara.

Backup

O event store é a fonte de verdade. Teste backup, point-in-time recovery e restauração.

Observabilidade

Registre stream ID, expected version, eventos gravados, posição global, duração e conflito.

Métricas

Monitore:

  • append por segundo;
  • conflitos de versão;
  • tempo de reidratação;
  • tamanho médio do stream;
  • snapshot hit rate;
  • lag de projeção;
  • falhas de upcast;
  • rebuild em andamento.

Consulte Métricas Prometheus no Node.js.

Logs

Não registre payload completo. Use tipos, versões e IDs. Consulte Logs com Pino no Node.js.

Testes de agregado

test('não cancela pedido enviado', () => {
  const order = rehydrateOrder([
    orderCreated(),
    orderShipped()
  ]);

  assert.throws(
    () => order.cancel('cliente solicitou'),
    /não pode ser cancelado/
  );
});

Teste de eventos emitidos

Aplique o histórico, execute command e compare os novos eventos, não o banco.

Teste de concorrência

Duas gravações com a mesma expected version devem resultar em apenas uma confirmação.

Teste de upcaster

Mantenha fixtures de todas as versões históricas.

Teste de projeção

Reprocesse o mesmo evento duas vezes e confirme idempotência.

Teste de rebuild

Reconstrua uma base e compare com a projeção atual por contagem e checksum.

Quando usar?

  • histórico é parte do domínio;
  • auditoria detalhada é necessária;
  • regras dependem da sequência;
  • novas projeções são valiosas;
  • investigação temporal é importante;
  • domínio é complexo e bem entendido.

Quando evitar?

  • CRUD simples;
  • equipe sem experiência operacional;
  • histórico não agrega valor;
  • requisitos de exclusão são incompatíveis;
  • não há estratégia de versionamento;
  • projeções não podem tolerar lag.

Erros comuns

  • Eventos sem versão: schemas antigos quebram.
  • Sem expected version: concorrência corrompe o stream.
  • Evento como comando: fato deixa de ser imutável.
  • Payload enorme: armazenamento e replay ficam caros.
  • Projeção não idempotente: redelivery duplica dados.
  • Snapshot como verdade: perda impede recuperação.
  • Adotar por moda: complexidade não gera benefício.

Boas práticas

  • Use eventos no passado.
  • Mantenha schema explícito.
  • Controle versão do stream.
  • Grave eventos atomicamente.
  • Versione eventos.
  • Use upcasters.
  • Torne projeções idempotentes.
  • Crie snapshots apenas quando necessário.
  • Meça lag e reidratação.
  • Teste rebuild e restauração.

Conclusão

O Event Sourcing no Node.js transforma fatos de domínio em fonte de verdade. O estado atual pode ser reconstruído, auditado e projetado em modelos diferentes.

O poder vem acompanhado de responsabilidade operacional. Versões, concorrência, snapshots, privacidade e projeções precisam ser planejados desde o início. Quando o histórico possui valor real, Event Sourcing oferece uma base rica para domínios complexos; quando não possui, uma modelagem relacional comum costuma ser mais simples e segura.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita