01 - Conceito (Separação Leitura vs Escrita)
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 |