CQRS com Minimal API : Conceitos, cenários e armadilhas

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
UpdateProduct

Você começa a pensar em:

ActivateProduct
ApplyDiscount
ChangeProductPrice

Ou 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	            Alta

CQRS 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
Estrutura

Instalação básica

Crie um projeto:

dotnet new web -n CqrsMinimalApi
cd CqrsMinimalApi

Se quiser usar EF Core:

dotnet add package Microsoft.EntityFrameworkCore.InMemory

1. 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
}
Adicionando um produto

Listar produtos

GET /products
Obtendo lista de produtos

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.

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente