System DesignMedium
What is CQRS and when does it actually help?
CQRS = Command Query Responsibility Segregation. Reads and writes use different models (and sometimes different stores).
Write side — commands change state:
public sealed record PlaceOrderCommand(Guid CustomerId, List<Item> Items);
public class PlaceOrderHandler(IOrderRepository repo) {
public async Task<Guid> HandleAsync(PlaceOrderCommand cmd, CancellationToken ct) {
var order = Order.Create(cmd.CustomerId, cmd.Items); // business rules
await repo.InsertAsync(order, ct);
return order.Id;
}
}
Read side — queries return DTOs optimised for the view:
public sealed record OrderSummaryDto(Guid Id, string CustomerName, decimal Total, int ItemCount);
public class OrderListQuery(AppDbContext db) {
public Task<List<OrderSummaryDto>> ListAsync() =>
db.Orders.Select(o => new OrderSummaryDto(
o.Id, o.Customer.Name, o.Total, o.LineItems.Count)).ToListAsync();
}
The read side may skip the domain model entirely — straight SQL → DTO.
When CQRS helps:
- Read and write workloads have very different shapes (writes are 1% of traffic but go through complex domain rules; reads are 99% and need different aggregations)
- Different scaling needs (read replicas; cached projections)
- The domain model is too heavyweight for simple list pages (loading aggregate roots just to display a name)
When CQRS is overkill:
- A small CRUD app — the duplication adds friction without payoff
- A single team owning everything — the separation rarely justifies itself
Two flavours:
- Light CQRS — same database, separate query/command code paths. Costs almost nothing, sometimes worth it.
- Heavy CQRS — separate read store, event-sourced writes, projection workers. Multi-quarter investment; reserve for genuinely complex domains.
Test: if your read queries already feel awkward against the write model (lots of mapping, fetching aggregates just to read one field), CQRS is probably earning its keep.