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_atNo Event Sourcing:
order_events
stream_id | version | event_type | payload | occurred_atO 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-842Todos 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
- Criar
order_view_v2. - Reprocessar todo o histórico.
- Acompanhar novos eventos.
- Comparar resultados.
- Trocar queries.
- 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.



