CQRS — Command Query Responsibility Segregation
CQRS splits the model you use to change state (the write side, driven by commands) from the model you use to read state (the read side, optimised for queries). They can have different shapes, different storage, even different scaling. It is the architectural cousin of CQS, and it pays off when reads and writes have genuinely different needs — but it adds moving parts, so apply it where that tension is real.
In a single shared model, the same entities serve both "change this" and "show me this", and you end up compromising both: the write model is cluttered with display concerns, and queries are awkward joins over a normalised schema. CQRS accepts that these are different jobs and gives each its own model. Commands validate business rules and produce state changes; queries return purpose-built read models (often denormalised) and never change anything.
CQRS is frequently confused with CQS (a method-level principle) and with Event Sourcing (a storage approach). They compose well but are independent: you can do CQRS without event sourcing, and CQS everywhere without either. This page is about the system-level split; see also Event-Driven Architecture and Distributed Systems & Consistency.
Separate the two responsibilities
- DoModel commands as explicit intentions (`SubmitKycCase`, `ApprovePayment`) that go through one write path enforcing all business rules and invariants. Commands return success/failure, not data to display.
- DoModel queries as read-only requests that return denormalised, purpose-shaped DTOs. A query never mutates state and never enforces a business rule — it just reads (see CQS).
- DoLet the read and write models diverge in shape. The write model protects invariants; the read model is whatever makes the screen or report fast. Forcing one shape on both is what CQRS exists to avoid.
- ConsiderSeparate read storage (a denormalised table, a search index, a cache) updated by projecting from the write side, when query load or shape justifies it. Don't add a second store until you need one.
- NeverLetting a query handler perform writes, or a command handler become a thinly disguised data-fetch. That collapses the separation and reintroduces the problems CQRS solves.
// Same EF entity used to enforce rules AND render a dashboard:
var c = db.Customers.Include(x => x.Cases).Include(x => x.Documents)
.Include(x => x.Screenings).First(x => x.Id == id);
c.RiskLevel = Recompute(c); // a write, mid-query, on a giant graph
The model is overfetched for display yet also mutated for rules. Both jobs are done badly and the write is hidden inside a read.
// write side: small, invariant-protecting
await handler.Handle(new RecomputeRisk(customerId));
// read side: flat, fast, read-only DTO
CustomerDashboardDto dto = await queries.GetDashboard(customerId);
Changing state and showing state are separate paths with separate models. Each is simple, and the write is explicit rather than smuggled into a read.
Adopt it where the tension is real
- DoReach for CQRS where reads vastly outnumber writes, where read and write shapes differ sharply, or where they must scale independently — dashboards and reporting over a complex domain are the classic case.
- AvoidFull CQRS with separate stores for simple CRUD. The split adds handlers, mapping, and eventual-consistency handling that a basic admin screen does not need.
- ConsiderA lightweight middle ground first: one database, but distinct command handlers and read-only query objects/DTOs. You get most of the clarity with little of the operational cost.
Mind the consistency gap
- DoWhen the read model is updated asynchronously from the write model, design for eventual consistency: the UI may briefly show stale data after a command. Make that explicit ("processing") rather than assuming instant agreement (see Distributed Systems & Consistency).
- DoMake projections that build read models idempotent and rebuildable, so a lost or duplicated update can be replayed without corruption (see Data Integrity & Transactions).
- ConsiderReturning the new state directly from the command path when the user must see their own change immediately, rather than round-tripping through an eventually-consistent read model.
Self-review checklist
- AskDo my reads and writes genuinely have different shapes or scaling needs, or am I adding ceremony for its own sake?
- AskIs every command enforcing invariants, and every query strictly read-only?
- AskIf the read model is separate, can I rebuild it and is it idempotent?
- AskHave I designed the UX for the moment between a command succeeding and the read model catching up?