CQRS e Event Sourcing com TypeScript: Separando Leitura, Escrita e Histórico de Verdade
O problema que CRUD esconde de você
Uma tabela orders com 15 colunas. Alguém muda o status de um pedido de "pendente" para "cancelado". O UPDATE sobrescreve o valor anterior. Três meses depois, o financeiro pergunta: "esse pedido foi cancelado antes ou depois do pagamento ser confirmado?". Você não sabe. O banco guarda o estado atual, não a sequência de fatos que levou até ele.
CQRS (Command Query Responsibility Segregation) separa o modelo de escrita do modelo de leitura. Event Sourcing grava cada mudança de estado como um evento imutável, em vez de sobrescrever linhas. Juntos, resolvem o problema de auditoria e permitem otimizar leitura e escrita de forma independente.
Mas a combinação tem custo real: complexidade de infraestrutura, eventual consistency entre modelos, e uma curva de aprendizado que pega de surpresa quem trata os dois como "CRUD com eventos". Este post implementa ambos do zero em TypeScript, com código que roda, e mostra onde a coisa quebra.
CQRS vs. CRUD vs. Event Sourcing: quando usar cada um
| Critério | CRUD tradicional | CQRS (sem Event Sourcing) | CQRS + Event Sourcing |
|---|---|---|---|
| Complexidade de implementação | Baixa | Média | Alta |
| Auditoria completa de mudanças | Não (precisa de tabela auxiliar) | Não nativamente | Sim, por design |
| Performance de leitura otimizável | Limitada (mesmo modelo) | Alta (modelo de leitura separado) | Alta |
| Consistência | Imediata | Eventual ou imediata | Eventual |
| Quando faz sentido | Apps com menos de 5 entidades de domínio, CRUD puro | Leitura e escrita com padrões de acesso muito diferentes | Domínios onde o histórico de mudanças É o negócio (financeiro, logística, healthcare) |
| Quando é excesso | Nunca é excesso para CRUD simples | Apps pequenas sem diferença de carga leitura/escrita | Qualquer sistema onde você não precisa reconstruir estado a partir de eventos |
Se seu sistema é um cadastro de usuários com login e perfil, CQRS + Event Sourcing é overengineering. Se você está modelando transações financeiras, movimentação de estoque ou workflows com múltiplos estados intermediários, a combinação se paga.
Modelando eventos de domínio
Eventos descrevem fatos que já aconteceram. O nome sempre no passado: OrderCreated, PaymentConfirmed, OrderCancelled. Comece pelos tipos:
// src/events/order-events.ts
export interface DomainEvent {
readonly eventId: string;
readonly aggregateId: string;
readonly eventType: string;
readonly timestamp: Date;
readonly version: number;
readonly payload: Record<string, unknown>;
}
export interface OrderCreated extends DomainEvent {
eventType: "OrderCreated";
payload: {
customerId: string;
items: Array<{ productId: string; quantity: number; priceInCents: number }>;
totalInCents: number;
};
}
export interface PaymentConfirmed extends DomainEvent {
eventType: "PaymentConfirmed";
payload: {
paymentId: string;
amountInCents: number;
method: "credit_card" | "pix" | "boleto";
};
}
export interface OrderCancelled extends DomainEvent {
eventType: "OrderCancelled";
payload: {
reason: string;
cancelledBy: string;
};
}
export type OrderEvent = OrderCreated | PaymentConfirmed | OrderCancelled;
O campo version é o número sequencial do evento dentro do aggregate. Ele serve para detectar conflitos de concorrência: se dois comandos tentam gravar a versão 3 ao mesmo tempo, o segundo falha. Sem isso, você perde eventos.
Event Store: gravando fatos imutáveis
O event store é um append-only log. Nada é atualizado, nada é deletado. A implementação mínima em memória (para entender o contrato) e depois a interface para persistência real:
// src/store/event-store.ts
import { randomUUID } from "node:crypto";
import type { DomainEvent } from "../events/order-events.js";
export class EventStore {
// Map de aggregateId para lista ordenada de eventos
private streams = new Map<string, DomainEvent[]>();
async append(
aggreg
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/cqrs-event-sourcing-typescript-guia-pratico](https://www.vivodecodigo.com.br/backend/cqrs-event-sourcing-typescript-guia-pratico)