Queries não são todas iguais: entenda onde cada uma deveria estar
Uma dúvida que aparece o tempo todo quando estamos falando de arquitetura em .net com DDD, CQRS ou qualquer abordagem que tente manter o domínio limpo é:
Onde eu coloco minhas queries?
E a resposta curta é: depende do tipo de query que você está fazendo.
Entendendo os dois tipos de leitura
Antes de decidir onde colocar uma query, vale entender que tipo de leitura ela representa. Eu costumo separar assim:
-
Queries que reidratam entidades do domínio
São operações que trazem uma ou mais entidades completas, com toda a carga de dados necessária para que comportamentos possam ser invocados. Essas vão para o repositório. -
Queries que retornam projeções
São operações de leitura que visam exibir dados: tabelas, cards, gráficos, listas, contagens.
Aqui você não precisa (e nem deve) montar entidades — e sim trabalhar com DTOs específicos da projeção. Essas ficam no Query Handler, ou em um serviço separado de leitura.
Modelos de leitura e de escrita
Essa distinção nos leva naturalmente ao conceito de modelos distintos para leitura e escrita — um dos pilares do CQRS, mas que também pode ser adotado de forma mais pragmática em projetos sem full CQRS.
Estratégia 1 – Leitura com EF, escrita com domínio rico
Você usa um único DbContext com Entity Framework para leitura e escrita, mas organiza o código de forma que cada modelo cumpra seu papel:
-
Leitura: acontece nos QueryHandlers, com LINQ direto sobre o DbContext, usando
.AsNoTracking()e projetando DTOs específicos para a tela. Aqui não há envolvimento do domínio, e nem precisa. -
Escrita: começa com a reidratação de entidades (via repositório ou
DbContext.Find), aplica os comportamentos diretamente sobre as entidades (que encapsulam as regras do domínio), e só então persiste as alterações comSaveChanges().
Ou seja, você reidrata, executa um comportamento (como account.Withdraw(amount)), e o EF detecta as mudanças e salva.
Vantagens:
- Não precisa separar projetos ou ORMs.
- Preserva o encapsulamento do domínio.
- Simplifica o início da adoção de DDD.
Ideal para projetos pequenos ou times ainda se adaptando à separação mais clara entre leitura e escrita.
Estratégia 2 – Leitura com Dapper, escrita com EF
Aqui você separa fisicamente os ORMs:
- EF continua sendo usado para escrita, respeitando o domínio.
- Leitura acontece com Dapper, direto em SQL, retornando DTOs prontos para a tela.
Vantagens:
- Mais performance nas queries.
- Mais controle sobre os joins e projections.
- Evita o antipattern do "repositório gigante com tudo dentro".
Estratégia 3 – Separação no banco: instância de leitura vs. escrita
Você pode ir além e usar instâncias separadas de banco para leitura e escrita:
- Escrita vai para o banco primário.
- Leitura é feita a partir de um read replica, sincronizado (quase em tempo real).
Ideal em sistemas de alta escala, onde leitura representa 90% das operações e não pode competir com os locks e I/O da escrita.
Requer cuidado com consistência eventual e com a sincronização de dados.
Estratégia 4 – Escrita NoSQL, leitura SQL
Uma abordagem menos comum, mas útil em cenários específicos:
- Escrita em NoSQL (como MongoDB), com flexibilidade de schema e performance para eventos, logs, documentos.
- Leitura em banco relacional (como Postgres ou SQL Server), com ETL que transforma os dados em um modelo relacional ideal para relatórios, dashboards e analytics.
Funciona bem em sistemas event-driven ou com pipelines de dados consolidados.
O que evitar
-
Jogar todas as queries dentro do mesmo repositório genérico (
IRepository<T>)
→ Isso acopla a leitura ao modelo de escrita e quebra o princípio de responsabilidade única. -
Tentar reidratar entidades para alimentar uma tela
→ Você está usando canhão para matar mosquito. Uma projeção resolve com muito menos custo e muito mais clareza.
Conclusão
Separar as leituras das escritas é sobre clareza de propósito:
- Se eu vou executar comportamento → reidrato a entidade via repositório.
- Se eu só vou mostrar na tela → projeto um DTO direto no handler.
E conforme o sistema cresce, você pode (e deve) escalar essa separação com estratégias como Dapper, bancos separados e pipelines de leitura otimizados.
Leitura é responsabilidade demais para cair tudo no seu repositório. Dê a ela o lugar certo no seu sistema.