rcRacoelho
HomeBlogDesafiosProjetosSetupVagasComunidade
rcRacoelho

Conteúdo sobre desenvolvimento, tecnologia e desafios de programação para impulsionar sua carreira em tech.

Links rápidos
  • Blog
  • Desafios
  • Projetos
  • Setup
  • Vagas
  • Comunidade
  • Links
Conecte-se
    © 2024–2026 Racoelho. Todos os direitos reservados.v3.0.27 · build 2026-10-04
    rcRacoelho
    HomeBlogDesafiosProjetosSetupVagasComunidade
    blog / Arquitetura de Sistemas / cqrs-na-prtica-como-separar-leitura-e-escrita-transforma-sua-arquitetura
    Arquitetura de Sistemas

    CQRS na Prática: Como Separar Leitura e Escrita Transforma Sua Arquitetura

    Rafael Coelho
    23 mar 2026 · 14 min de leitura

    Introdução

    Você já tentou fazer uma query complexa num sistema que tem um monte de regra de negócio no mesmo modelo? Tipo, você precisa listar pedidos com status, nome do cliente, último pagamento, é o total com desconto aplicado. Aí você vai la, faz um JOIN de 5 tabelas, filtra por 3 condições, e reza pro Entity Framework não gerar um SQL de 200 linhas.

    O problema tá no fato de que você tá usando o mesmo modelo pra duas coisas completamente diferentes: gravar dados com regras de negócio e ler dados pra exibir na tela. É isso, mais cedo ou mais tarde, vira um monstro.

    É exatamente isso que o CQRS resolve. CQRS significa Command Query Responsibility Segregation, que em bom português é: separar a responsabilidade de quem escreve da responsabilidade de quem lê. Parece simples, mas muda completamente a forma como você pensa arquitetura.


    O Problema do Modelo Único

    Vamos começar pelo cenário clássico. Você tem uma aplicação de e-commerce. Tem la sua entidade Pedido:

    csharp
    public class Pedido { public Guid Id { get; set; } public Guid ClienteId { get; set; } public Cliente Cliente { get; set; } public List<ItemPedido> Itens { get; set; } public decimal Total { get; set; } public decimal Desconto { get; set; } public StatusPedido Status { get; set; } public DateTime CriadoEm { get; set; } public DateTime? AtualizadoEm { get; set; } public Pagamento Pagamento { get; set; } public Endereco EnderecoEntrega { get; set; } }

    Essa entidade serve pra tudo: criar pedido, atualizar status, calcular frete, aplicar cupom, listar pedidos do cliente, gerar relatório de vendas...

    Agora pensa comigo: quando você cria um pedido, você precisa validar estoque, calcular preço, aplicar regras de desconto. Você precisa de toda a complexidade do domínio.

    Mas quando você lista pedidos na tela do admin? Você só precisa de meia dúzia de campos. Número do pedido, nome do cliente, valor total, status. Pronto.

    Usar o mesmo modelo pros dois casos e forçar um sapato número 38 num pe 42. Funciona? Até funciona. Mas dói.

    Os problemas que aparecem:

    • Queries ficam lentas porque o modelo de domínio não foi feito pra ser consultado
    • O modelo fica inchado com propriedades que só existem pra leitura
    • Mudanças no domínio quebram as queries e vice-versa
    • Otimizar leitura significa poluir o modelo de escrita com índices, campos calculados, desnormalizações

    O Que é CQRS, Afinal?

    CQRS é a ideia de que comandos (operações que mudam estado) e queries (operações que leem estado) devem ser tratados por modelos separados.

    Visualmente, é isso:

    Do lado esquerdo, você tem os Commands: operações que alteram o estado do sistema. Criar pedido, cancelar pedido, aplicar desconto.

    Do lado direito, você tem as Queries: operações que só leem dados. Listar pedidos, buscar detalhes, gerar relatório.

    Cada lado tem seu próprio modelo, otimizado pro que ele faz de melhor.


    O Lado da Escrita: Commands

    O lado de escrita é onde mora a complexidade do negócio. Aqui você tem validações, regras, invariantes.

    Um Command é basicamente uma intenção. "Eu quero criar um pedido". "Eu quero cancelar esse pedido". É uma instrução, não uma pergunta.

    csharp
    public class CriarPedidoCommand { public Guid ClienteId { get; set; } public List<ItemDto> Itens { get; set; } public string CupomDesconto { get; set; } public Guid EnderecoEntregaId { get; set; } }

    Percebe que o Command não tem Id, não tem Total, não tem Status. Ele só carrega a intenção e os dados necessários pra executar.

    Quem processa é o Command Handler:

    csharp
    public class CriarPedidoHandler : ICommandHandler<CriarPedidoCommand> { private readonly IPedidoRepository _repository; private readonly IEstoqueService _estoque; private readonly ICupomService _cupons; public async Task Handle(CriarPedidoCommand cmd) { // 1. Validar estoque foreach (var item in cmd.Itens) { var disponivel = await _estoque.VerificarDisponibilidade( item.ProdutoId, item.Quantidade ); if (!disponivel) throw new EstoqueInsuficienteException(item.ProdutoId); } // 2. Aplicar cupom (se houver) decimal desconto = 0; if (!string.IsNullOrEmpty(cmd.CupomDesconto)) { desconto = await _cupons.CalcularDesconto( cmd.CupomDesconto, cmd.Itens ); } // 3. Criar o pedido com as regras de dominio var pedido = Pedido.Criar( cmd.ClienteId, cmd.Itens.Select(i => new ItemPedido(i.ProdutoId, i.Quantidade, i.Preco)), desconto, cmd.EnderecoEntregaId ); // 4. Persistir await _repository.Salvar(pedido); // 5. Reservar estoque await _estoque.Reservar(pedido.Itens); } }

    O handler faz uma coisa: processa a intenção. Ele valida, aplica regras, persiste. Sem se preocupar com como os dados vao ser exibidos depois.

    O modelo de escrita e normalizado, rico em comportamento, focado em proteger as regras de negócio.


    O Lado da Leitura: Queries

    Agora o outro lado. Aqui a gente não precisa de regra de negócio nenhuma. A gente só precisa de dados prontos pra exibir.

    O modelo de leitura pode ser completamente diferente do modelo de escrita:

    csharp
    // Modelo de leitura - otimizado pra exibicao public class PedidoResumoView { public Guid Id { get; set; } public string NumeroPedido { get; set; } public string NomeCliente { get; set; } public int QuantidadeItens { get; set; } public decimal ValorTotal { get; set; } public string Status { get; set; } public DateTime Data { get; set; } }

    Percebe: sem navegação pra Cliente, sem lista de Itens, sem Pagamento. Só o que a tela precisa. Flat, simples, direto.

    E a query pode ir direto no banco com SQL otimizado:

    csharp
    public class ListarPedidosQuery : IQueryHandler<ListarPedidosRequest, List<PedidoResumoView>> { private readonly IDbConnection _readDb; public async Task<List<PedidoResumoView>> Handle(ListarPedidosRequest request) { var sql = @" SELECT p.id, p.numero_pedido AS NumeroPedido, c.nome AS NomeCliente, p.quantidade_itens AS QuantidadeItens, p.valor_total AS ValorTotal, p.status AS Status, p.criado_em AS Data FROM pedidos_view p INNER JOIN clientes c ON c.id = p.cliente_id WHERE (@Status IS NULL OR p.status = @Status) ORDER BY p.criado_em DESC OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY"; return (await _readDb.QueryAsync<PedidoResumoView>(sql, new { request.Status, request.Offset, request.Limit })).ToList(); } }

    Sem ORM pesado, sem lazy loading, sem N+1. Query limpa, rápida, otimizada.

    Você pode até usar um banco diferente pro lado de leitura. Um banco relacional pro lado de escrita (que precisa de transações e consistência) é um banco de documentos ou até um cache pro lado de leitura (que precisa de velocidade).


    Sincronização: Como os Dois Lados Conversam?

    Se você tem dois modelos separados (e potencialmente dois bancos separados), como o lado de leitura fica atualizado quando o lado de escrita muda?

    A resposta: eventos.

    Quando um comando e processado e altera o estado, ele publica um evento. Esse evento e consumido por um handler que atualiza o modelo de leitura.

    O código do projetor (ou denormalizador) seria algo assim:

    csharp
    public class PedidoProjector : IEventHandler<PedidoCriadoEvent>, IEventHandler<PedidoCanceladoEvent>, IEventHandler<PedidoPagoEvent> { private readonly IDbConnection _readDb; public async Task Handle(PedidoCriadoEvent evt) { await _readDb.ExecuteAsync(@" INSERT INTO pedidos_view (id, numero_pedido, cliente_id, quantidade_itens, valor_total, status, criado_em) VALUES (@Id, @NumeroPedido, @ClienteId, @QuantidadeItens, @ValorTotal, 'Criado', @CriadoEm)", new { evt.PedidoId, evt.NumeroPedido, evt.ClienteId, evt.QuantidadeItens, evt.ValorTotal, evt.CriadoEm }); } public async Task Handle(PedidoCanceladoEvent evt) { await _readDb.ExecuteAsync(@" UPDATE pedidos_view SET status = 'Cancelado', atualizado_em = @AtualizadoEm WHERE id = @PedidoId", new { evt.PedidoId, evt.AtualizadoEm }); } public async Task Handle(PedidoPagoEvent evt) { await _readDb.ExecuteAsync(@" UPDATE pedidos_view SET status = 'Pago', atualizado_em = @AtualizadoEm WHERE id = @PedidoId", new { evt.PedidoId, evt.AtualizadoEm }); } }

    Cada evento sabe exatamente o que mudar no modelo de leitura. Simples, rápido, desacoplado.


    Eventual Consistency: O Elefante na Sala

    Quando você separa escrita e leitura com eventos no meio, você abre mao de consistência imediata. O dado foi salvo no banco de escrita, mas o modelo de leitura pode levar alguns milissegundos (ou segundos) pra refletir a mudança.

    Isso se chama consistência eventual (eventual consistency). E significa que, por um breve período, o lado de leitura pode estar "desatualizado".

    Na prática, pra maioria dos sistemas, isso não é um problema. Pensa no Instagram: quando você posta uma foto, ela aparece pra você na hora, mas pode levar alguns segundos pra aparecer no feed dos seus seguidores. Você nem percebe.

    Mas tem casos onde isso importa. Sistemas financeiros, por exemplo, onde você precisa garantir que o saldo tá correto na hora da transação. Nesses casos, você pode:

    1. Manter consistência forte no que importa: a escrita valida tudo no momento da transação
    2. Aceitar eventual consistency no que não importa: o relatório de vendas pode ter 2 segundos de atraso sem problema
    3. Usar patterns como Read-your-writes: apos um comando, forçar a leitura do banco de escrita por um curto período

    A chave e entender: você não precisa de consistência imediata em tudo. Você precisa de consistência imediata onde o negócio exige. No resto, eventual consistency é totalmente aceitavel.


    Arquitetura Prática: E-Commerce com CQRS

    Vamos montar uma arquitetura real. Um e-commerce com catálogo, pedidos e pagamentos.

    Estrutura de Pastas

    code
    src/ |-- ECommerce.Commands/ | |-- Pedidos/ | | |-- CriarPedidoCommand.cs | | |-- CriarPedidoHandler.cs | | |-- CancelarPedidoCommand.cs | | |-- CancelarPedidoHandler.cs | |-- Pagamentos/ | | |-- ProcessarPagamentoCommand.cs | | |-- ProcessarPagamentoHandler.cs | |-- Catalogo/ | |-- AtualizarEstoqueCommand.cs | |-- AtualizarEstoqueHandler.cs | |-- ECommerce.Queries/ | |-- Pedidos/ | | |-- ListarPedidosQuery.cs | | |-- DetalhesPedidoQuery.cs | | |-- RelatorioPedidosQuery.cs | |-- Catalogo/ | | |-- BuscarProdutosQuery.cs | | |-- ProdutoDetalheQuery.cs | |-- ViewModels/ | |-- PedidoResumoView.cs | |-- PedidoDetalheView.cs | |-- ProdutoCatalogoView.cs | |-- ECommerce.Domain/ | |-- Pedidos/ | | |-- Pedido.cs | | |-- ItemPedido.cs | | |-- Events/ | | |-- PedidoCriadoEvent.cs | | |-- PedidoCanceladoEvent.cs | |-- Catalogo/ | |-- Produto.cs | |-- ECommerce.Projections/ | |-- PedidoProjector.cs | |-- CatalogoProjector.cs | |-- ECommerce.API/ |-- Controllers/ |-- PedidosCommandController.cs |-- PedidosQueryController.cs

    Os Controllers

    Percebe que até os controllers sao separados:

    csharp
    // Controller de ESCRITA [ApiController] [Route("api/pedidos")] public class PedidosCommandController : ControllerBase { private readonly IMediator _mediator; [HttpPost] public async Task<IActionResult> Criar([FromBody] CriarPedidoCommand cmd) { await _mediator.Send(cmd); return Accepted(); // 202 - o pedido ta sendo processado } [HttpPost("{id}/cancelar")] public async Task<IActionResult> Cancelar(Guid id) { await _mediator.Send(new CancelarPedidoCommand { PedidoId = id }); return Accepted(); } } // Controller de LEITURA [ApiController] [Route("api/pedidos")] public class PedidosQueryController : ControllerBase { private readonly IMediator _mediator; [HttpGet] public async Task<IActionResult> Listar([FromQuery] ListarPedidosRequest request) { var result = await _mediator.Send(new ListarPedidosQuery(request)); return Ok(result); } [HttpGet("{id}")] public async Task<IActionResult> Detalhes(Guid id) { var result = await _mediator.Send(new DetalhesPedidoQuery { PedidoId = id }); return Ok(result); } }

    O controller de escrita retorna 202 Accepted porque a operação pode ser assíncrona. O de leitura retorna 200 OK com os dados.

    Pipeline Completo

    O fluxo completo de criar um pedido:

    1. Cliente envia POST /api/pedidos com os dados
    2. Command Controller recebe e despacha CriarPedidoCommand
    3. Command Handler valida estoque, calcula preço, aplica regras
    4. Dominio cria a entidade Pedido e registra PedidoCriadoEvent
    5. Repository salva no banco de escrita
    6. Event Bus publica PedidoCriadoEvent
    7. Projector recebe o evento e atualiza pedidos_view no banco de leitura
    8. Query Controller pode agora retornar o pedido atualizado

    Cada peça faz uma coisa. Cada peça pode ser testada isoladamente. Cada peça pode escalar independentemente.


    Quando Usar CQRS?

    CQRS não e bala de prata. Tem cenários onde faz total sentido e cenários onde é overengineering.

    Faz sentido quando:

    • Leitura e escrita tem volumes muito diferentes. Ex: um e-commerce tem 100x mais leituras que escritas. Você pode escalar o lado de leitura separadamente.
    • Os modelos de leitura e escrita sao muito diferentes. Se a tela precisa de dados de 5 tabelas joinadas, ter um modelo de leitura desnormalizado resolve.
    • Você precisa de performance em leitura. Com modelos de leitura otimizados, você pode usar cache, bancos especializados, materialized views.
    • O domínio é complexo. Se o lado de escrita tem muitas regras, separar permite focar nelas sem poluir com concerns de exibição.
    • Você já usa Event Sourcing. CQRS é o parceiro natural de Event Sourcing.

    Não faz sentido quando:

    • CRUD simples. Se sua aplicação é basicamente um formulário que salva e lista, CQRS é um canhão pra matar formiga.
    • Time pequeno sem experiência. CQRS adiciona complexidade. Se o time não entende os trade-offs, vai virar um problema.
    • Consistência imediata e obrigatória em tudo. Se você não pode aceitar nenhum delay entre escrita e leitura, CQRS com bancos separados vai te dar dor de cabeça.
    • Projeto pequeno e de curta duração. O overhead não compensa.

    CQRS Sem Bancos Separados: O Meio Termo

    Um ponto importante: CQRS não exige bancos separados. Você pode começar simples.

    Nível 1: Separação lógica. Mesmo banco, mesmas tabelas, mas com modelos e handlers separados pra leitura e escrita. Já ganha muito em organização.

    Nível 2: Views ou tabelas desnormalizadas. Mesmo banco, mas com materialized views ou tabelas de leitura. Ganha performance sem a complexidade de sincronização.

    Nível 3: Bancos separados com eventos. O CQRS completo. Máximo de flexibilidade e escalabilidade, mas também máximo de complexidade.

    A minha recomendação: comece pelo nível 1. Separe os modelos e handlers. Quando sentir que precisa de mais performance na leitura, evolua pro nível 2. Só va pro nível 3 quando realmente precisar.


    Comparativo Rápido: Com e Sem CQRS

    AspectoSem CQRSCom CQRS
    ModelosUm modelo pra tudoModelos separados por responsabilidade
    QueriesORM com joins complexosSQL otimizado em modelos flat
    EscritaMistura regras com concerns de exibiçãoFocada puramente em regras de negócio
    Performance de leituraLimitada pelo modelo de domínioOtimizada com desnormalização
    ComplexidadeMenor no inícioMaior no início, menor a longo prazo
    EscalabilidadeEscala tudo juntoEscala leitura e escrita independentemente
    TestabilidadeTestes acopladosTestes isolados por lado

    Erros Comuns ao Implementar CQRS

    Pra fechar, alguns erros que eu vejo bastante:

    1. Aplicar CQRS em tudo. Você não precisa de CQRS no CRUD de cadastro de usuário. Aplique onde tem complexidade real.

    2. Ignorar o custo da eventual consistency. Se você não comunicar pro time e pro produto que "o dado pode levar alguns segundos pra aparecer", vai ter problema.

    3. Complicar demais a sincronização. Comece com algo simples. Um evento, um projetor, uma tabela de leitura. Não tente montar uma arquitetura de microservicos com Kafka no primeiro dia.

    4. Não monitorar a defasagem. Se o modelo de leitura tá 30 segundos atrasado, você precisa saber. Monitore o lag entre escrita e leitura.

    5. Achar que CQRS e Event Sourcing. CQRS é sobre separar modelos. Event Sourcing é sobre persistir eventos como fonte de verdade. Sao complementares, mas independentes.


    Conclusão

    CQRS é uma daquelas coisas que, quando você entende, você começa a enxergar onde faz sentido em todo sistema que você olha. A separação de leitura e escrita e tao natural que você se pergunta por que não pensou nisso antes.

    Mas lembra: comece simples. Separe os modelos. Separe os handlers. Sinta a dor antes de trazer a complexidade. Quando a performance de leitura virar problema, você já vai ter a base pra evoluir sem reescrever tudo.

    Neste artigo
    • Introdução
    • O Problema do Modelo Único
    • O Que é CQRS, Afinal?
    • O Lado da Escrita: Commands
    • O Lado da Leitura: Queries
    • Sincronização: Como os Dois Lados Conversam?
    • Eventual Consistency: O Elefante na Sala
    • Arquitetura Prática: E-Commerce com CQRS
    • Quando Usar CQRS?
    • CQRS Sem Bancos Separados: O Meio Termo
    • Comparativo Rápido: Com e Sem CQRS
    • Erros Comuns ao Implementar CQRS
    • Conclusão

    Leia tambémContinue lendo

    18 min

    Como Funciona um Sistema de Buscas de Verdade (e por que LIKE não é busca)

    Arquitetura de Sistemas
    9 min

    Como Arquitetar um Sistema de Notificações que Aguenta Escala

    Arquitetura de Sistemas
    11 min

    Seu Sistema Está Lento? O Problema Não É o Banco de Dados — É Quando Você Processa os Dados

    Arquitetura de Sistemas
    rcRacoelho

    Conteúdo sobre desenvolvimento, tecnologia e desafios de programação para impulsionar sua carreira em tech.

    Links rápidos
    • Blog
    • Desafios
    • Projetos
    • Setup
    • Vagas
    • Comunidade
    • Links
    Conecte-se
      © 2024–2026 Racoelho. Todos os direitos reservados.v3.0.27 · build 2026-10-04