Principio: CQRS separa o modelo de escrita (commands) do modelo de leitura (queries). Event Sourcing armazena eventos em vez de estado β€” o estado e derivado ao reproduzir os eventos.

Command Query Responsibility Segregation

CQRS e o principio de que o modelo usado para escrever dados (commands) deve ser diferente do modelo usado para ler dados (queries). Cada lado pode ser otimizado, escalado e evoluido independentemente.

Fluxo: Commands vs Queries

WritesReadsPersistFetchEventsUpdateClientCommandCreate / Update / DeleteQueryGet / List / SearchWrite ModelNormalized DBRead ModelDenormalized ViewsSync / Projection

Por que separar leitura e escrita?

AspectoLeitura (Query)Escrita (Command)
Escala10-100x mais frequente. Replicas read-only.Menos frequente, exige consistencia forte.
OtimizacaoViews denormalizadas, caching agressivo.Modelo normalizado, regras de negocio complexas.
ModeloDTOs flat, materialized views.Aggregates ricos, domain events, invariantes.
SegurancaPermissoes mais liberais.Permissoes restritas, auditoria detalhada.

Tradicional CRUD vs CQRS

AspectoCRUD TradicionalCQRS
ModeloUnico β€” mesmo modelo para ler e escreverSeparado β€” write model + read model
EscalabilidadeEscala junto (read e write competem pelo mesmo recurso)Escala independente (read replica, cache, denormalizacao)
ComplexidadeBaixa β€” simples e diretoAlta β€” mais infraestrutura e codigo
ConsistenciaForte β€” leitura imediataEventual β€” read model pode estar atrasado

Commands β€” alteracoes de estado

Commands representam intencoes de mudar o estado do sistema. Sao imperativos: "crie pedido", "adicione item", "cancele pedido". Nao retornam dados β€” retornam void ou no maximo um Id.

// Commands β€” intencoes de alterar estado public record CreateOrderCommand(Guid CustomerId, List<OrderItemDto> Items); public record UpdateOrderCommand(Guid OrderId, List<OrderItemDto> Items); public record CancelOrderCommand(Guid OrderId, string Reason); // Handler retorna void ou Id β€” nunca retorna dados de leitura public class CreateOrderHandler { public async Task<Guid> HandleAsync(CreateOrderCommand cmd, CancellationToken ct) { var order = Order.Create(cmd.CustomerId); foreach (var item in cmd.Items) order.AddItem(item.ProductName, item.Qty, item.Price); await _repo.SaveAsync(order, ct); return order.Id; } }

Queries β€” leitura de dados

Queries nao alteram estado. Retornam DTOs otimizados para a tela que vai consumir. Podem usar views denormalizadas, caches, ou bancos de leitura separados.

// Queries β€” leitura de dados, sem side effects public record GetOrderQuery(Guid OrderId); public record GetOrdersListQuery(Guid? CustomerId, int Page, int PageSize); // DTOs otimizados para a tela public record OrderDto(Guid Id, string CustomerName, decimal Total, string Status, DateTime CreatedAt); // Handler le do read model (view, cache, ou DB separado) public class GetOrderQueryHandler { public async Task<OrderDto?> HandleAsync(GetOrderQuery query, CancellationToken ct) => await _readDb.OrderSummaries .Where(o => o.OrderId == query.OrderId) .Select(o => new OrderDto(o.OrderId, o.CustomerName, o.Total, o.Status, o.CreatedAt)) .FirstOrDefaultAsync(ct); }
Importante: CQRS nao exige Event Sourcing. Voce pode usar CQRS com um unico banco de dados β€” basta separar os modelos de leitura e escrita no codigo. Event Sourcing e uma estrategia de persistencia ortogonal.
Quando CQRS vale a complexidade extra?
CQRS vale quando: 1) Read e write tem requisitos de escala muito diferentes. 2) O modelo de dominio e complexo e voce quer proteger invariantes no write side. 3) Voce precisa de projecoes otimizadas para diferentes consumidores (API, relatorio, busca). Nao vale para CRUDs simples β€” a complexidade adicional nao se justifica.