CQRS com Minimal API : Conceitos, cenários e armadilhas
Descubra como CQRS melhora organização, performance e clareza no backend usando uma abordagem leve com Minimal APIs.
Introdução
Dentro do ecossistema .NET, é muito comum começarmos o desenvolvimento das nossas aplicações com uma abordagem simples: um único modelo de domínio responsável por leitura e escrita. Isso funciona bem no início, mas à medida que a aplicação cresce, tanto em volume de dados quanto em complexidade de regras, esse modelo começa a apresentar limitações.
É nesse contexto que o CQRS (Command Query Responsibility Segregation) entra como uma alternativa arquitetural. Ele propõe algo simples, porém poderoso: separar completamente as operações de leitura (queries) das operações de escrita (commands).
Com a chegada das Minimal APIs no ASP.NET Core, ficou ainda mais fácil implementar esse padrão de forma leve, sem a necessidade de estruturas pesadas como Controllers ou camadas excessivas.
O que é CQRS
CQRS é um padrão de arquitetura de software que divide a aplicação em dois fluxos distintos:
- Command (escrita) → altera estado (create, update, delete)
- Query (leitura) → apenas consulta dados, sem efeitos colaterais
A principal ideia é: ler e escrever são problemas totalmente diferentes e devem ser tratados de formas diferentes.
Modelo tradicional (CRUD)
No modelo tradicional:
public class ProductService
{
public Product GetById(int id);
public void Create(Product product);
public void Update(Product product);
}Aqui nesse exemplo, o mesmo modelo atende tanto para leitura quanto para escrita.
Modelo CQRS
No CQRS, você separa de forma distinta cada operação:
// Command
public record CreateProductCommand(string Name, decimal Price);
// Query
public record GetProductByIdQuery(int Id);Cada um com possui o seu handler:
public class CreateProductHandler
{
public void Handle(CreateProductCommand command) { }
}
public class GetProductByIdHandler
{
public Product Handle(GetProductByIdQuery query) { }
}CQRS não é sobre tecnologia é sobre modelo mental
Outro ponto importante: CQRS não depende de framework.
Você pode implementar com:
- Minimal API
- Controllers
- MediatR
- Sem nenhuma biblioteca externa
O valor está na forma de pensar, não na ferramenta.
Esse modelo mental traz algumas mudanças importantes:
1. Pensar em ações, não em CRUD
Em vez de:
CreateProduct
UpdateProductVocê começa a pensar em:
ActivateProduct
ApplyDiscount
ChangeProductPriceOu seja, comandos mais próximos da realidade do negócio.
2. Pensar em respostas específicas
Em vez de:
GetProduct()Você passa a ter:
GetProductDetails()
GetProductSummary()
GetProductsForDashboard()Cada query atende um cenário específico.
3. Redução de acoplamento acidental
Quando leitura e escrita compartilham o mesmo modelo, qualquer mudança impacta tudo.
Com CQRS:
- Alterações em queries não afetam commands
- Alterações no domínio não quebram leitura (se bem projetado)
O custo real do CQRS
Apesar das vantagens, CQRS tem um custo que precisa ser entendido.
1. Mais código
Você passa a ter:
- Commands
- Queries
- Handlers separados
Isso aumenta o número de arquivos e abstrações existentes dentro do seu projeto
2. Maior esforço cognitivo
Para quem entra no projeto:
- Não existe mais “um lugar só” para tudo
- Precisa entender o fluxo de command vs query para localizar cada coisa
3. Overengineering
Se aplicado cedo demais:
- Complica sem necessidade
- Dificulta manutenção
- Reduz produtividade
Command não é “um método de escrita”
Um erro comum é tratar command como apenas um DTO para insert/update. Conceitualmente falando, isso não reflete sua verdadeira funcionalidade.
Um Command representa uma intenção de mudança de estado, não apenas uma operação técnica.
Exemplo:
public record UpdateProductPriceCommand(int Id, decimal NewPrice);Aqui você não está dizendo “atualize esse registro”, mas sim:
“eu quero alterar o preço desse produto”
Essa diferença parece pequena e quase imperceptivel, mas muda completamente a forma como você modela o sistema:
- Você pode validar regras antes da mudança
- Pode rejeitar comandos inválidos
- Pode auditar intenções
- Pode evoluir comportamento sem quebrar contrato
Ou seja, o command está muito mais próximo do domínio do que da infraestrutura.
Query não é “um select genérico”
Da mesma forma, uma query não deveria retornar entidades completas por padrão.
O objetivo da query é responder uma pergunta específica da forma mais eficiente possível.
Exemplo:
public record ProductListItemDto(string Name, decimal Price);Aqui você está modelando a resposta para um caso de uso e não reutilizando uma entidade.
Isso permite:
- Evitar overfetching(mais dados que o necessário)
- Melhorar performance
- Reduzir acoplamento
- Customizar formato por endpoint
No CQRS, queries são orientadas a casos de uso, não a entidades.
Por que usar CQRS?
1. Escalabilidade
Você pode escalar leitura e escrita de maneira individual. Em sistemas com muitas consultas (ex: dashboards), isso faz muita diferença.
2. Performance
Queries podem usar projeções otimizadas (DTOs, views, SQL direto com Dapper), sem depender do modelo definido no domínio.
3. Separação de responsabilidades
Cada operação tem sua própria lógica isolada, isso torna a aplicação muito mais fácil de manter e evoluir.
4. Flexibilidade
Você pode usar tecnologias diferentes para leitura e escrita (ex: EF Core para escrita, Dapper para leitura).
Quando usar CQRS
CQRS não é a tecnologia que vai resolver tudo, é importante saber quais os cenários onde ele é melhor aplicado para você conseguie o máximo de desempenho:
Bons cenários
- Sistemas com alta carga de leitura (relatórios, dashboards)
- Domínios complexos (regras de negócio pesadas)
- Sistemas distribuídos ou baseados em eventos
- Aplicações que precisam evoluir com independência entre leitura e escrita
Cenários onde pode ser exagero
- CRUD simples
- Aplicações pequenas
- Sistemas sem complexidade de domínio
Se o seu sistema é basicamente um CRUD, CQRS pode mais atrapalhar do que ajudar.
CQRS com Minimal API: por que faz sentido?
Minimal APIs são ideais para CQRS porque:
- Reduzem boilerplate
- Facilitam organização por feature
- Funcionam bem com handlers simples
- Integram naturalmente com DI
Você consegue implementar CQRS sem precisar de frameworks adicionais como MediatR (embora possa usar, se quiser).
Comparação com outras abordagens
CQRS vs CRUD tradicional
Aspecto CRUD tradicional CQRS
Simplicidade Alta Média
Escalabilidade Baixa Alta
Organização Média Alta
Performance Média AltaCQRS vs uso com MediatR
MediatR é frequentemente usado com CQRS, mas não é obrigatório.
Com MediatR
- Mais desacoplamento
- Pipeline behaviors (logging, validação)
- Mais abstração
Sem MediatR (como aqui)
- Mais simples
- Menos indireção
- Mais controle direto
Para sistemas menores/médios, evitar MediatR pode ser uma decisão mais pragmática.
Erros comuns ao usar CQRS
1. Usar CQRS para tudo
Aplicar CQRS em um CRUD simples só aumenta complexidade sem ganho real.
2. Não separar de verdade
Exemplo errado:
public class ProductService
{
public Product Get(int id);
public void Create(Product product);
}Isso não é CQRS, é só um service tradicional.
3. Reutilizar entidades de escrita na leitura
Na leitura, prefira DTOs:
public record ProductDto(string Name, decimal Price);4. Ignorar performance nas queries
CQRS permite otimizar leitura, mas isso só funciona se você realmente fizer isso.
Exemplo com Dapper seria mais performático que EF em alguns cenários.
5. Complexidade excessiva
Separar demais (ex: 1 handler por micro-operação irrelevante) pode tornar o sistema difícil de navegar.
Exemplo prático completo
Vamos montar um cenário real e aplicar o que aprendemos até aqui com as seguintes funções:
- Criar produto (Command)
- Listar produtos (Query)
Estrutura sugerida
Uma estrutura simples:
/Features
/Products
/Commands
CreateProductCommand.cs
CreateProductHandler.cs
/Queries
GetProductQuery.cs
GetProductHandler.cs
Instalação básica
Crie um projeto:
dotnet new web -n CqrsMinimalApi
cd CqrsMinimalApiSe quiser usar EF Core:
dotnet add package Microsoft.EntityFrameworkCore.InMemory1. Modelo
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = "";
public decimal Price { get; set; }
}2. DbContext
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
}3. Command (CreateProduct)
public record CreateProductCommand(string Name, decimal Price);Handler:
public class CreateProductHandler
{
private readonly AppDbContext _context;
public CreateProductHandler(AppDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateProductCommand command)
{
var product = new Product
{
Name = command.Name,
Price = command.Price
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return product.Id;
}
}4. Query (GetAllProducts)
public record GetAllProductsQuery();Handler:
public class GetAllProductsHandler
{
private readonly AppDbContext _context;
public GetAllProductsHandler(AppDbContext context)
{
_context = context;
}
public async Task<List<Product>> Handle()
{
return await _context.Products
.AsNoTracking()
.ToListAsync();
}
}5. Program.cs (Minimal API)
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseInMemoryDatabase("db"));
builder.Services.AddScoped<CreateProductHandler>();
builder.Services.AddScoped<GetAllProductsHandler>();
var app = builder.Build();
// Command
app.MapPost("/products", async (
CreateProductCommand command,
CreateProductHandler handler) =>
{
var id = await handler.Handle(command);
return Results.Created($"/products/{id}", id);
});
// Query
app.MapGet("/products", async (
GetAllProductsHandler handler) =>
{
var products = await handler.Handle();
return Results.Ok(products);
});
app.Run();Testando a API
Criar produto
POST /products
{
"name": "Teclado",
"price": 100
}
Listar produtos
GET /products
Evoluções possíveis
A partir desse exemplo simples, você pode evoluir para:
- Validação com FluentValidation
- Pipeline de logging
- Uso de MediatR
- Event sourcing
- Banco separado para leitura
- Cache (Redis) para queries
Conclusão
CQRS é um padrão poderoso, mas que deve ser aplicado com critério. Ele resolve problemas reais de escalabilidade, organização e performance, especialmente em sistemas mais complexos.
Com Minimal APIs, ficou muito mais simples adotar CQRS sem introduzir peso desnecessário na aplicação. Você consegue manter a arquitetura limpa, previsível e fácil de evoluir.
A chave é saber quando usar:
- Se o sistema é simples → mantenha simples
- Se começa a crescer em complexidade → CQRS pode ser o próximo passo natural
No fim, CQRS não é sobre separar código é sobre separar responsabilidades de forma inteligente sabendo separar essas responsabilidades de forma aderquada, você ganha:
- Clareza arquitetural
- Melhor capacidade de evolução
- Mais controle sobre performance
Mas isso vem com um custo, e a maturidade está em saber quando esse custo se justifica.
No contexto de aplicações modernas em .NET, especialmente com Minimal APIs, CQRS deixa de ser um padrão “pesado” e passa a ser uma ferramenta prática, que pode ser aplicada de forma incremental e consciente.
Código-fonte do projeto
Para facilitar o entendimento, disponibilizei o projeto completo com o exemplo de CQRS, já organizado com separação de responsabilidades e boas práticas:
👉 Baixar o projeto no GitHub neste link:
Quer ver mais conteúdos como este?
Se você trabalha com .NET, C# e backend, este blog seguirá trazendo conteúdos práticos e objetivos sobre o ecossistema Microsoft, com foco em código, boas práticas e decisões técnicas aplicadas ao mundo real.
Acompanhe para continuar aprofundando seu domínio sobre a plataforma .NET.