Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - Conceito (Separação Leitura vs Escrita)

#cqrs#cqs#escrita#leitura
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

CQRS: Separação entre Leitura e Escrita

CQRS, Command Query Responsibility Segregation, é o padrão que separa os modelos usados para atualizar dados dos modelos usados para lê-los. Em vez de uma única estrutura de classes e tabelas servindo às duas necessidades, o lado de comando trabalha com um modelo otimizado para proteger regras de negócio, e o lado de consulta trabalha com modelos otimizados para devolver dados no formato que as telas e relatórios consomem. A separação responde a uma assimetria estrutural: escrita precisa de invariantes rígidas e tráfego moderado, leitura precisa de flexibilidade e escala alta. Em produção, CQRS aparece desde formatos leves, com dois modelos sobre o mesmo banco, até formatos completos, com bases físicas distintas sincronizadas por eventos. O padrão não exige Event Sourcing, embora os dois se complementem, e entender o que muda em cada grau de separação é o que evita adoções superficiais ou exageradas.

Origem no CQS

A raiz do padrão está no CQS, Command Query Separation, proposto por Bertrand Meyer no contexto de métodos. O princípio afirma que cada método deve ser ou um comando, que muda o estado observável e não retorna nada, ou uma consulta, que retorna algo e não muda estado. Métodos que fazem os dois, como o clássico pop de uma pilha, que remove e devolve o elemento, são a exceção que o princípio tenta eliminar, porque misturar efeitos e retornos complica raciocínio, testes e concorrência.

O exemplo em código deixa a fronteira visível. O comando executa a intenção e responde apenas com confirmação; a consulta devolve o dado e não toca em nada.

// Comando: muda o estado e não devolve dado de negócio
public void adicionarItem(String pedidoId, ItemDto item) {
    pedidos.aplicar(new AdicionarItemAoPedido(pedidoId, item.sku(), item.quantidade()));
}

// Consulta: devolve dado e não muda estado
public List<ItemDto> itensDoPedido(String pedidoId) {
    return resumoDao.buscarItens(pedidoId);   // apenas leitura do read model
}

No nível de métodos, CQS é disciplina de design. CQRS dá o passo seguinte, elevando a separação da unidade de método para a arquitetura: modelos distintos, caminhos distintos e, em graus maiores, serviços e bancos distintos. A letra R a mais, Responsibility, marca justamente essa ampliação de escopo.

A violação clássica do CQS ajuda a entender o ganho. Métodos que consultam e modificam ao mesmo tempo parecem convenientes e se revelam armadilhas: quem chama não sabe se o retorno reflete o estado antes ou depois da mudança, testes precisam de mais cenários, e leituras acidentais em caminhos de escrita impedem otimizações simples. No plano da arquitetura, a mesma confusão aparece em endpoints que gravam e devolvem a entidade completa, forçando o backend a calcular a resposta dentro da transação e o frontend a depender de um formato que serve a nenhuma tela direito.

Lado de comando e lado de consulta

O lado de comando concentra tudo que decide. Recebe intenções, valida regras, carrega agregados, aplica invariantes e grava a mudança. Seu modelo de dados é a representação que torna essas regras expressíveis e defensáveis, e sua API é pequena, com verbos do negócio e retornos mínimos. Erros são esperados e fazem parte do contrato: um comando pode ser recusado, e a recusa é informação de primeira classe.

O lado de consulta concentra tudo que exibe. Recebe perguntas, lê modelos desenhados para a resposta e devolve DTOs prontos, sem lógica de negócio e sem efeitos colaterais. Seus modelos podem ser desnormalizados, duplicados e espelhados de várias formas, porque nada ali protege invariantes. O número de modelos de leitura tende a crescer com a variedade de telas, e isso é saudável: cada visão serve a um propósito de consulta específico.

A fronteira entre os lados é onde o design acontece. Uma operação entra por exatamente um deles; se precisa das duas coisas, ela se decompõe em comando seguido de consulta. Essa disciplina elimina uma classe inteira de endpoints que gravam e devolvem dados recalculados, que são historicamente o ponto onde transações, caches e contratos de API se enroscam.

Em termos de organização de código, a separação muda coisas concretas.

  • Comandos vivem em pacotes próprios, com objetos de comando, handlers e repositórios de agregado, e não expõem nada além de confirmação ou erro.
  • Consultas vivem em outro pacote, com objetos de consulta, handlers de leitura e DAOs sobre o read model, sem tocar no modelo de domínio.
  • O modelo de domínio deixa de ser o DTO da API; entidades deixam de ter getters anotados para serialização e o acoplamento entre banco, domínio e contrato de rede se rompe.
  • Testes se dividem naturalmente: regras nos testes de comando, forma do dado nos testes de consulta, sem mistura de fixtures.

A assimetria de carga entre leitura e escrita

A separação se justifica porque os dois lados têm perfis de tráfego e requisitos que raramente combinam em um único modelo.

Característica Lado de escrita Lado de leitura
Volume típico Dezenas a centenas por segundo Centenas a dezenas de milhares por segundo
Invariantes Rígidas, protegidas por agregados Nenhuma, dados são visões derivadas
Forma do modelo Normalizada, centrada no domínio Desnormalizada, centrada na tela
Consistência Transacional, imediata Eventual, com defasagem controlada
Escala Limitada por conflito de negócio Horizontal, por réplicas e caches
Testes Regras de negócio e concorrência Forma do dado e performance de consulta

Um e-commerce ilustra a proporção: um pedido recebe poucas escritas, mas é lido dezenas de vezes, pelo cliente acompanhando o status, pelo sistema de logística, pelo suporte e pelo relatório diário. Modelar as duas demandas em uma única estrutura força a escrita a carregar a forma da tela e a leitura a pagar o preço das invariantes. Com CQRS, a escrita protege o pedido, e a leitura vira quantas visões forem necessárias.

A assimetria também muda com o tempo, e isso é argumento a favor da separação gradual. Campanhas sazonais multiplicam leitura sem mover escrita; fechamentos mensais multiplicam relatórios; um novo aplicativo de parceiros pode multiplicar leitura por dez. Com lados separados, esses picos são absorvidos escalando a leitura, que é barata e sem estado, e a escrita, que concentra o valor, fica protegida da tempestade de consultas.

Graus de separação

CQRS não é um interruptor liga-desliga, é uma rampa. O custo cresce a cada degrau, e a escolha consciente do degrau é o que diferencia adoções bem-sucedidas de exageros.

Grau Descrição Custo adicional Quando usar
Mesmo modelo, caminhos separados Handlers de comando e query distintos, um único modelo de domínio Mínimo, apenas organização de código Começo da disciplina, sistemas pequenos
Modelos separados, mesmo banco Escrita em tabelas normalizadas, leitura em views ou tabelas derivadas na mesma base Baixo, sincronização na mesma transação Assimetria de consulta sem infraestrutura nova
Bases físicas distintas Read model em outra tecnologia, sincronizado por eventos ou CDC Médio, exige pipeline e consistência eventual Escala de leitura alta ou tecnologia de busca especializada
Serviços separados Escrita e leitura em serviços independentes, escalados e versionados à parte Alto, operação distribuída completa Escalas e times distintos, domínios grandes

A maior parte dos sistemas se beneficia dos dois primeiros degraus e nunca precisa do terceiro. Subir de degrau sem pressão de carga ou de tecnologia é custo sem retorno, e descer um degrau é quase sempre possível quando a assimetria desaparece.

Um detalhe prático no degrau de bases distintas: o read model não precisa ser relacional, e essa é metade do ganho do grau. Visões de busca textual vão para um motor de busca, agregações pesadas vão para um warehouse, sessões e totais quentes vão para um cache. O lado de comando continua relacional e estável, e a leitura escolhe a ferramenta por pergunta, e não por conveniência única. É esse direito de escolha que a separação abre, e é por isso que times com muitos formatos de consulta adotam CQRS mesmo sem Event Sourcing.

CQRS sem Event Sourcing

Uma confusão comum é imaginar que CQRS depende de Event Sourcing. Não depende. A versão mais simples do padrão roda sobre um banco relacional convencional: o lado de comando grava em tabelas normalizadas com todas as invariantes, e o lado de leitura consome tabelas derivadas, views materializadas ou dados espelhados, atualizados na mesma transação da escrita ou por gatilhos e jobs.

Nessa configuração, a sincronização é o ponto sensível. Atualizar a visão de leitura dentro da mesma transação do comando garante consistência imediata a custo de escrita maior; publicar a mudança por eventos ou capturar dados alterados desloca o custo e introduz defasagem, comprando escala e desacoplamento. As duas respostas são válidas, e a escolha segue o orçamento de latência da leitura.

O mecanismo de captura de mudanças, conhecido como CDC, merece destaque na transição. Ferramentas leem o log de transações do banco de escrita e aplicam as alterações no read model, sem código adicional no domínio. É um caminho de baixo atrito para adotar o terceiro grau de separação sobre sistemas já existentes, com defasagem de segundos e a mesma exigência de idempotência das projeções. A diferença conceitual é importante: o CDC replica dados, enquanto os eventos de domínio carregam semântica; visões construídas sobre CDC refletem tabelas, e visões construídas sobre eventos refletem fatos.

-- Escrita: transação atualiza o modelo normalizado e a visão derivada
BEGIN;

UPDATE pedidos SET status = 'PAGO', pago_em = now() WHERE id = $1;

-- Manutenção da leitura na mesma transação, no grau 2 de separação
UPDATE pedidos_resumo
   SET status = 'PAGO', pago_em = now()
 WHERE pedido_id = $1;

COMMIT;

O exemplo mostra a essência do degrau intermediário: duas formas de dado, uma transação única. É simples de operar e suficiente para muitos sistemas; a defasagem só aparece quando a visão passa para outra base, e aí os padrões de consistência eventual do módulo entram em cena.

A combinação com Event Sourcing entra quando o lado de escrita adota o log de eventos como verdade. Nesse arranjo, as projeções substituem as tabelas derivadas, e a reconstrução de leitura a partir do log passa a ser possível. Os princípios de comando e consulta permanecem os mesmos; muda a mecânica de sincronização e o poder de reprocessamento. O arquivo sobre a combinação completa aprofunda esse ponto.

O que CQRS não é

Aclarar os limites do padrão evita decisões equivocadas de adoção e críticas injustas.

  • CQRS não é uma arquitetura de microsserviços; a separação existe igualmente dentro de um único processo, com pacotes e handlers distintos.
  • CQRS não exige duas bases de dados; os dois primeiros graus de separação rodam em uma única base com modelos lógicos diferentes.
  • CQRS não é sincronização bidirecional; a leitura nunca escreve no modelo de comando, e a direção é uma só, do comando para a consulta.
  • CQRS não é uma camada a mais obrigatória em todo CRUD; aplicações simples com carga equilibrada ganham pouco e podem não valer o custo.
  • CQRS não substitui transações; o lado de comando continua dependendo das garantias transacionais do armazenamento para proteger invariantes.

Erros comuns e boas práticas

  • Separar por moda antes de medir assimetria; sistemas com carga equilibrada e consultas simples pagam o custo sem receber o benefício.
  • Deixar o handler de consulta chamar o agregado para montar a resposta, quebrando a fronteira e recriando o acoplamento que o padrão remove.
  • Duplicar lógica de negócio no read model; regra vive no lado de comando, e a leitura apenas materializa efeitos.
  • Ignorar a defasagem no desenho da API; o contrato de leitura precisa deixar claro que o dado é eventualmente consistente.
  • Criar um read model por tela sem critério, explodindo o número de visões a manter; agrupe por tipo de consulta e frequência de uso.
  • Esquecer de versionar DTOs de leitura publicados para outros sistemas; o contrato de leitura é tão público quanto o de comando.
  • Deixar de tratar falha de sincronização do read model; quando a visão atrasa ou quebra, o time precisa de sinal, e não de clientes reclamando primeiro.
  • Trocar o modelo de escrita para atender uma consulta pontual; a exceção justificada vira read model, e não buraco na fronteira.

Um detalhe de operação fecha o quadro: monitore os dois lados com painéis separados. Latência de comando, taxa de conflito e tamanho de transação de um lado; latência de consulta, lag de sincronização e taxa de erro de DTO do outro. Sem essa partição, os alertas se misturam e o incidente de leitura é tratado como problema de escrita, perdendo o diagnóstico rápido que a separação poderia oferecer.

Práticas que consolidam o padrão: mantenha comandos e consultas em pacotes distintos, com dependências visíveis em uma única direção; nomeie comandos no imperativo e consultas com prefixo de leitura, tornando a fronteira legível no código; meça separadamente latência de comando e de consulta, porque os orçamentos são diferentes; e revise o grau de separação a cada ciclo de capacidade, subindo apenas quando a medida justificar. Complete com documentação viva do mapa de read models, porque a leitura tende a crescer em número e sem mapa o time perde a noção de quanto custa manter cada visão.

Sintoma em produção Causa provável Mitigação
Endpoints lentos de listagem em picos Consulta pesando no modelo de escrita Criar read model dedicado com desnormalização das colunas usadas
Regras de negócio duplicadas entre telas e backend Lógica infiltrada nos handlers de consulta Migrar a regra para o comando e deixar o DTO só com efeitos
Escrita degradada por índices de relatório Mesma base servindo os dois lados Mover consultas analíticas para base de leitura separada
Frontend exibindo estado antigo após operação Defasagem não tratada no contrato Adotar read-your-own-writes e estados de pendência na UI
Onboarding confuso com classes espalhadas Fronteira de comando e consulta invisível no código Padronizar pacotes, nomenclatura e revisão de dependências