CQRS—Command Query Responsibility Segregation—sounds intimidating, but the core idea is simple. Separate your read operations from your write operations. Here’s why you’d want to do that and how to get started.
The Problem
In traditional CRUD applications, the same model handles both reads and writes. This works fine until:
- Your read queries need different data shapes than your write models
- Reads vastly outnumber writes and have different performance needs
- You need to scale reads and writes independently
- Your domain model is complex but your read views are simple
The CQRS Solution
Split your system into two sides:
- Commands: Operations that change state (Create, Update, Delete)
- Queries: Operations that return data without side effects (Read)
// Command side - rich domain model
public class PlaceOrderCommand
{
public Guid CustomerId { get; set; }
public List Lines { get; set; }
}
public class PlaceOrderHandler
{
public async Task Handle(PlaceOrderCommand command)
{
var customer = await _customerRepo.GetById(command.CustomerId);
var order = customer.PlaceOrder(command.Lines);
await _orderRepo.Save(order);
}
}
// Query side - simple DTOs optimized for reads
public class OrderSummaryQuery
{
public Guid OrderId { get; set; }
}
public class OrderSummaryHandler
{
public async Task Handle(OrderSummaryQuery query)
{
// Direct database query, no domain model
return await _db.QueryFirstAsync (
"SELECT Id, Total, Status FROM Orders WHERE Id = @Id",
new { query.OrderId });
}
}
When to Use CQRS
CQRS adds complexity. Don’t use it for simple CRUD apps. Consider it when:
- Read and write workloads are significantly different
- You need to scale reads and writes independently
- Your domain is complex but read views are simple
- You’re using Event Sourcing (they pair well together)
Implementation with MediatR
MediatR is a popular library that makes CQRS easy in .NET:
// Command
public class CreateUserCommand : IRequest
{
public string Email { get; set; }
public string Name { get; set; }
}
// Handler
public class CreateUserHandler : IRequestHandler
{
public async Task Handle(CreateUserCommand request, CancellationToken ct)
{
var user = new User(request.Email, request.Name);
await _repository.Add(user);
return user.Id;
}
}
// Controller
[HttpPost]
public async Task> Create(CreateUserCommand command)
{
var id = await _mediator.Send(command);
return Ok(id);
}
References
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.