O CQRS no Node.js separa operações que alteram estado das operações que apenas consultam dados. Commands representam intenções como criar pedido, cancelar pagamento ou alterar endereço. Queries retornam informações sem produzir efeitos de negócio.
Essa separação pode simplificar domínios complexos, permitir modelos de leitura otimizados e escalar escrita e consulta de forma diferente. Porém, CQRS adiciona componentes, consistência eventual e necessidade de sincronizar projeções. Em sistemas CRUD simples, o custo geralmente não se justifica.
Neste guia, você aprenderá commands, handlers, queries, modelos de leitura, eventos, outbox, projeções, idempotência, autorização, observabilidade e testes.
O que é CQRS?
CQRS significa Command Query Responsibility Segregation. A referência CQRS de Martin Fowler explica a ideia. A documentação CQRS Pattern da Microsoft apresenta benefícios e custos.
Para eventos confiáveis, consulte Outbox Pattern no Node.js. Para workflows distribuídos, veja Saga Pattern no Node.js.
Command
Um command expressa uma intenção:
{
"type": "CreateOrder",
"commandId": "...",
"customerId": "91",
"items": [
{ "productId": "42", "quantity": 2 }
]
}Use nomes no imperativo. Um command pode ser rejeitado por validação, autorização ou regra de negócio.
Query
Uma query pede dados:
{
"type": "GetOrderDetails",
"orderId": "842"
}Ela não deve alterar estado observável. Métricas técnicas e logs não contam como efeito de negócio.
Separação de handlers
class CreateOrderHandler {
constructor({ repository, eventStore }) {
this.repository = repository;
this.eventStore = eventStore;
}
async execute(command) {
// validar, autorizar e alterar estado
}
}
class GetOrderDetailsHandler {
constructor({ readModel }) {
this.readModel = readModel;
}
async execute(query) {
return this.readModel.findById(query.orderId);
}
}CQRS não exige microsserviços
Você pode manter commands e queries no mesmo processo e banco, apenas separando código e responsabilidades.
CQRS não exige Event Sourcing
CQRS pode usar tabelas relacionais tradicionais. Event Sourcing é uma opção adicional, não uma obrigação.
Modelo de escrita
O modelo de escrita protege invariantes:
- estoque não negativo;
- pedido não cancelado após entrega;
- limite de crédito;
- transições de status;
- autorização do ator.
Modelo de leitura
O modelo de leitura é otimizado para a interface:
CREATE TABLE order_details_view (
order_id BIGINT PRIMARY KEY,
customer_name TEXT NOT NULL,
status TEXT NOT NULL,
total_cents BIGINT NOT NULL,
item_count INTEGER NOT NULL,
last_updated_at TIMESTAMPTZ NOT NULL
);Ele pode duplicar dados para evitar joins caros.
Mesma base de dados
Uma implementação inicial pode atualizar o modelo de leitura na mesma transação. Isso mantém consistência forte, mas acopla as estruturas.
Bancos separados
Em maior escala, escrita e leitura podem usar bancos diferentes. Eventos atualizam as projeções de forma assíncrona.
Consistência eventual
Depois de um command retornar sucesso, a query pode demorar alguns milissegundos ou segundos para refletir a alteração.
Read-your-writes
Para melhorar a experiência, o command pode retornar dados essenciais:
{
"orderId": "842",
"status": "created",
"version": 1
}A interface mostra esse resultado enquanto a projeção atualiza.
Token de consistência
Você pode retornar uma versão ou posição do evento. A query aguarda até a projeção alcançar esse token, com timeout.
202 Accepted
Commands assíncronos podem retornar 202 e uma URL de operação:
HTTP/1.1 202 Accepted
Location: /api/operations/abc123Command bus
class CommandBus {
constructor() {
this.handlers = new Map();
}
register(type, handler) {
this.handlers.set(type, handler);
}
async execute(command, context) {
const handler = this.handlers.get(command.type);
if (!handler) {
throw new Error(`Handler ausente: ${command.type}`);
}
return handler.execute(command, context);
}
}Um bus local organiza dispatch, mas não deve esconder fluxo e erros.
Query bus
Um query bus semelhante pode aplicar tracing, autorização e métricas. Evite criar abstração genérica difícil de depurar.
Validação
Valide o schema na entrada:
const createOrderSchema = z.object({
commandId: z.string().uuid(),
customerId: z.string().min(1),
items: z.array(z.object({
productId: z.string().min(1),
quantity: z.number().int().positive()
})).min(1)
});Autorização
O handler precisa validar se o ator pode executar a intenção. Não confie apenas na rota ou interface.
Idempotência de commands
Commands podem ser repetidos por timeout. Use commandId ou chave de idempotência.
Consulte Idempotência em APIs Node.js.
Transação do command
await withTransaction(pool, async client => {
const existing = await findProcessedCommand(client, command.commandId);
if (existing) return existing.response;
const order = await createOrder(client, command);
await recordCommand(client, command.commandId, order);
await insertOutboxEvent(client, createOrderCreatedEvent(order));
return order;
});Eventos de domínio
Commands alteram agregados e produzem eventos:
{
"type": "OrderCreated",
"orderId": "842",
"version": 1,
"occurredAt": "..."
}Outbox
Grave evento e estado na mesma transação para evitar dual write.
Projeção
async function projectOrderCreated(event, client) {
await client.query(`
INSERT INTO order_details_view (
order_id,
customer_name,
status,
total_cents,
item_count,
last_updated_at
) VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (order_id)
DO NOTHING
`, [
event.orderId,
event.customerName,
'created',
event.totalCents,
event.itemCount,
event.occurredAt
]);
}Projeção idempotente
Eventos podem chegar novamente. Registre event ID ou use versionamento condicional.
Versão da projeção
UPDATE order_details_view
SET status = $1,
version = $2
WHERE order_id = $3
AND version = $4;Isso impede aplicar um evento fora de ordem.
Evento fora de ordem
Particione mensagens pelo aggregate ID ou armazene eventos pendentes até a versão anterior chegar.
Rebuild de projeção
Se você possui histórico de eventos, pode reconstruir o modelo de leitura:
- criar nova tabela;
- reprocessar eventos;
- validar contagens;
- trocar a leitura;
- remover a projeção antiga.
Sem Event Sourcing
Quando não há histórico completo, reconstrua a projeção consultando o banco de escrita em lotes.
Projeções múltiplas
Uma mesma sequência de eventos pode alimentar:
- detalhes de pedido;
- dashboard de vendas;
- histórico do cliente;
- índice de busca;
- relatório financeiro.
Query otimizada
Retorne DTOs específicos:
SELECT order_id,
customer_name,
status,
total_cents,
item_count
FROM order_details_view
WHERE order_id = $1;Não carregue o agregado de escrita apenas para montar uma tela.
Paginação
Modelos de leitura podem ter índices específicos para cursor e filtros. Consulte Paginação em APIs Node.js.
Cache
Queries podem usar cache, mas invalidar por evento e respeitar autorização. O command não deve depender de cache possivelmente antigo para validar invariantes.
Pesquisa textual
Uma projeção pode alimentar Elasticsearch ou outro mecanismo, enquanto o banco de escrita continua sendo a fonte de verdade.
Agregados
O modelo de escrita deve ter limites claros. Uma transação altera um agregado por vez sempre que possível.
Concorrência
Use versão do agregado e lock otimista:
UPDATE orders
SET status = $1,
version = version + 1
WHERE id = $2
AND version = $3;Veja Lock Otimista no Node.js.
Sagas
Commands que atravessam vários serviços podem iniciar uma saga. CQRS organiza entrada e leitura; saga coordena transações distribuídas.
Erros de negócio
Retorne códigos estáveis:
{
"code": "ORDER_CANNOT_BE_CANCELLED",
"message": "O pedido já foi enviado"
}Commands síncronos
O handler pode executar e retornar resultado na mesma requisição quando a operação é rápida.
Commands assíncronos
Para tarefas longas, grave o command, retorne 202 e processe em worker.
Fila de commands
Uma fila adiciona redelivery e atraso. O consumidor precisa de idempotência e observabilidade.
Prioridade
Não misture todos os commands em uma fila única se operações urgentes competem com lotes pesados.
Backpressure
Limite consumo conforme banco e dependências. Métricas de idade da fila mostram saturação.
Versionamento de commands
Commands internos também evoluem. Adicione versão e mantenha consumidores compatíveis durante deploy.
Versionamento de queries
Uma API pode evoluir seus DTOs usando versionamento de contrato. Consulte Versionamento de API no Node.js.
Multi-tenant
Commands e queries devem incluir tenant derivado da autenticação, não fornecido livremente pelo corpo.
Segurança do modelo de leitura
Duplicar dados não remove regras de acesso. A query precisa filtrar tenant, escopo e campos permitidos.
Observabilidade
Registre command/query type, handler, duração, resultado, erro, versão e correlation ID.
Métricas
Monitore:
- commands por tipo;
- falhas de negócio;
- falhas técnicas;
- latência de handlers;
- queries por tipo;
- lag das projeções;
- eventos pendentes;
- rebuilds.
Consulte Métricas Prometheus no Node.js.
Lag da projeção
Meça diferença entre event.occurredAt e o horário de aplicação. Esse indicador representa a experiência de consistência eventual.
Tracing
Crie spans para command handler, commit, outbox, publicação e projeção.
Logs
Não registre payload completo. Use IDs, tipo e resultado. Veja Logs com Pino no Node.js.
Testes unitários
Teste commands como regras de negócio e queries como transformação de dados.
Teste de command
test('rejeita cancelamento após envio', async () => {
const order = createOrderFixture({ status: 'shipped' });
await assert.rejects(
() => handler.execute({
type: 'CancelOrder',
orderId: order.id
}),
OrderCannotBeCancelledError
);
});Teste de projeção
Envie o mesmo evento duas vezes e confirme que o modelo permanece correto.
Teste de lag
Simule fila atrasada e confirme que a API informa estado processando ou usa o token de consistência.
Teste de rebuild
Reconstrua a projeção em uma base limpa e compare contagens e checksums.
Quando usar CQRS?
- regras de escrita complexas;
- leituras muito diferentes do domínio;
- escalas de leitura e escrita distintas;
- necessidade de múltiplas projeções;
- operações assíncronas;
- auditoria e eventos importantes.
Quando não usar?
- CRUD simples;
- equipe pequena sem necessidade;
- consistência imediata obrigatória em todas as telas;
- baixo volume;
- domínio ainda pouco entendido;
- infraestrutura sem observabilidade.
Adoção gradual
Comece separando classes de command e query no mesmo banco. Só crie projeção assíncrona quando houver benefício mensurável.
Erros comuns
- CQRS para todo CRUD: complexidade cresce sem retorno.
- Query alterando estado: semântica fica imprevisível.
- Command sem idempotência: retries duplicam efeitos.
- Projeção sem versão: eventos fora de ordem corrompem dados.
- Ignorar lag: usuários veem estado antigo sem explicação.
- Compartilhar modelo: leitura continua acoplada à escrita.
- Sem rebuild: projeção quebrada não pode ser recuperada.
Boas práticas
- Use commands no imperativo.
- Mantenha queries sem efeitos de negócio.
- Proteja invariantes no modelo de escrita.
- Crie DTOs de leitura específicos.
- Use outbox.
- Torne projeções idempotentes.
- Meça lag.
- Versione mensagens.
- Automatize rebuild.
- Adote gradualmente.
Conclusão
O CQRS no Node.js separa intenções de mudança das necessidades de consulta. Essa divisão permite um modelo de escrita focado em regras e modelos de leitura desenhados para cada interface.
O custo aparece em projeções, consistência eventual e infraestrutura. Com commands idempotentes, outbox, versões e métricas de lag, CQRS pode simplificar domínios complexos sem transformar cada operação simples em um sistema distribuído desnecessário.



