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

CQRS no Node.js: Guia Prático

Atualizado em: 26 de agosto de 2026

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

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/abc123

Command 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:

  1. criar nova tabela;
  2. reprocessar eventos;
  3. validar contagens;
  4. trocar a leitura;
  5. 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.

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