CQRS — Command Query Responsibility Segregation — separates the model used to change state from the models used to read it. One model enforces invariants on write; separate, denormalised models answer queries.
It is the most misapplied pattern in modern architecture, usually because it is adopted as a default rather than as a response to a specific problem. Most systems should not use it, and understanding why is more useful than understanding how.
The problem it addresses
A single model has to serve two jobs with opposite requirements.
Writes need invariants. An order must not be shipped before payment, a balance must not go negative, a booking must not double-allocate a room. Enforcing that requires a normalised, consistent, carefully bounded model.
Reads need shapes. A dashboard wants totals by region and month. A search page wants denormalised, indexed, filterable documents. A mobile client wants a summary with three joins pre-resolved.
Serving both from one model means compromise in both directions: the write model grows fields and denormalisations for the sake of queries, and the queries grow joins because the write model is normalised. As the two diverge, the compromise gets worse.
Diagram, as text
- Commands PlaceOrder, CancelOrder leads to Write model normalised, enforces invariants
- Write model normalised, enforces invariants leads to Write store
- Write store leads to Projection updates read models
- Projection updates read models leads to Read model: order summaries
- Projection updates read models leads to Read model: revenue by region
- Projection updates read models leads to Read model: search index
- Query: my orders leads to Read model: order summaries
- Query: dashboard leads to Read model: revenue by region
- Query: search leads to Read model: search index
What it genuinely buys
Read models can be shaped for their queries. A dashboard reads one pre-aggregated row instead of scanning and joining. This is the benefit that scales — reads usually outnumber writes by orders of magnitude.
The write model stays clean. It contains only what enforcing invariants requires, so it stays small and comprehensible.
Reads and writes scale independently. Different stores, different replicas, different technologies. A search index, a cache and a relational store can each serve the queries they are good at.
Adding a query does not touch the write model. A new read model is a new projection, built by replaying or by subscribing.
What it costs, and this is the part to weigh
Eventual consistency, and it is user-visible. Projections lag the write. A user who places an order and is redirected to a list that does not contain it will report a bug — and they are right to. This is the single biggest source of CQRS regret, and it is not an implementation detail you can optimise away.
Three mitigations, all imperfect:
- Read your own writes — route a user's immediate reads to the write model, or to a session-pinned view. Effective, and it puts a conditional in every read path.
- Return enough from the command to render the next screen without re-reading.
- Design the UI to not need it — optimistic display, or an explicit "processing" state. Often the best answer, and it is a product decision rather than a technical one.
More moving parts. Projections must be built, monitored, versioned and rebuilt. A stalled projection is a read model quietly serving stale data with no error anywhere.
Duplication is deliberate. The same fact exists in several shapes, and a projection bug means they disagree. You need a way to detect divergence and rebuild.
It is harder to reason about. "Where does this number come from?" has a longer answer than a query.
The degrees of separation
CQRS is not binary, and the cheap end of the range is where most of the value is.
Separate methods. Commands return nothing; queries have no side effects. This is command–query separation, it costs nothing, and it is good practice regardless.
Separate models, one store. Write through a domain model, read through purpose-built queries or views against the same database. No eventual consistency, most of the shape benefit, very little cost. This is the sweet spot and where most systems that "use CQRS" should stop.
Separate stores. Read models in a different store, updated asynchronously. Now you have eventual consistency and projection infrastructure — and the scaling and shape benefits that justify them.
Separate stores plus event sourcing. The full arrangement, with the largest cost.
Most teams jump to the third or fourth when the second would have solved their problem.
Check your understanding
Common mistakes
Adopting it as a default architecture. It is a response to a specific problem, not a starting point.
Taking CQRS and event sourcing together. Two unfamiliar things at once, each with its own failure modes.
Ignoring read-your-own-writes. Users see their action fail to appear and report it as a bug.
No monitoring of projection lag. A stalled projection serves stale data with no error anywhere.
No way to rebuild a projection. A bug in a projection is unrecoverable without one.
Jumping to separate stores. Separate models against one database solves most cases with no consistency lag.
Read models nobody uses. Each projection is permanent maintenance; build them for actual queries.
Business logic in projections. Projections transform shape; decisions belong in the write model.
Practice
An e-commerce team has a slow admin dashboard aggregating orders, revenue and inventory across six tables, taking 11 seconds. They propose full CQRS with a separate read store fed by events. Evaluate and propose.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
One slow query is named. Ask what the cheapest thing that fixes it is.
Show a strong answer
One slow query does not justify separate stores. The proposal brings eventual consistency, a projection pipeline, monitoring, rebuild tooling and a second datastore to operate — all to fix a page that admins load a few times a day. The cost is permanent and the benefit is narrow.
Establish the actual cause first, because eleven seconds across six tables has several possible explanations with very different fixes:
- A missing index on a join or filter column. Embarrassingly common, and it turns eleven seconds into a hundred milliseconds for no architectural change.
- An N+1 pattern in the code assembling the page, which no amount of read-model design fixes if it is reintroduced.
- A genuinely expensive aggregate over millions of rows. This is the only case where the shape of the data is really the problem.
Assuming it is the third, my proposal in order of cost:
- A materialised view or summary table in the same database, refreshed on a schedule — every five minutes is almost certainly acceptable for an admin dashboard, and I would confirm that with the people who use it rather than assume it. This is separate models against one store: the whole shape benefit, no eventual-consistency surprise for customers, no new infrastructure.
- Incremental maintenance if a scheduled refresh is too slow to compute: update the summary on write, in the same transaction. Still one store, still strongly consistent.
- A read replica if the dashboard's load is affecting transactional performance. That is a capacity problem with a capacity answer.
What I would tell the team about their proposal: the instinct — reads and writes want different shapes — is correct, and it is exactly what a summary table expresses. What they have added on top is asynchrony, and asynchrony is the expensive part. Nothing about the dashboard's problem requires it.
What would change my answer: if the dashboard were customer-facing at high volume, if several such aggregates existed and were all growing, or if the aggregation genuinely could not be maintained in the transactional store without hurting writes. Then a separate read store earns its cost — and even then I would want the first projection built and running before committing to the pattern everywhere.
One thing I would insist on regardless: a number. Eleven seconds is measured; the target should be too, along with how fresh the data must be. Those two numbers decide between every option above, and without them the discussion is about architecture preferences rather than about the dashboard.
A system uses CQRS with separate stores. Users complain that after saving a profile change, the profile page shows the old value for a few seconds. Propose fixes and pick one.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
This is read-your-own-writes. Each fix trades somewhere different.
Show a strong answer
This is the canonical CQRS complaint and the users are right — it is a bug from their perspective, whatever the architecture says. Four options, each trading somewhere different.
Option 1: route the user's reads to the write model for a short window after their write. Correct for the user who made the change, and everyone else keeps reading the fast projection.
- Trade: a conditional in the read path, and state to track "this user wrote recently" — usually a timestamp in the session or a cookie.
- Risk: the window is a guess. Too short and the problem returns intermittently, which is worse than consistently.
Option 2: return the updated profile from the command and render from that. No extra read at all.
- Trade: commands must return data, which some teams treat as a purity violation. It is not — returning the result of an operation is ordinary.
- Limit: only fixes the immediate screen. Navigating away and back within the lag window still shows stale data.
Option 3: optimistic UI. Show the new value immediately from what was submitted, reconcile when the projection catches up.
- Trade: the client now holds state that may be wrong if the write actually failed downstream, so it needs a reconciliation and error path.
- Strength: it also fixes navigation within the window, since the client holds the value.
Option 4: wait for the projection. The command returns a position, and the read blocks until the projection has caught up to it.
- Trade: reintroduces synchronous coupling and latency, which is a substantial amount of what CQRS was adopted to avoid.
- Where it fits: rare, high-value operations where staleness is genuinely unacceptable.
I would ship option 2 plus option 3. Returning the updated profile fixes the immediate screen with almost no machinery, and optimistic rendering covers navigation within the window. Together they address what users actually experience, and neither adds a conditional to every read path or couples reads back to the write side.
What I would not do: option 4 as a general policy. Blocking reads until projections catch up gives back the decoupling and the read scalability, which is most of the reason for the architecture.
What I would add regardless of the fix: a projection-lag metric with an alert. If lag is normally 200 ms and users see multiple seconds, the real problem may be a struggling projection rather than an inherent property of the design — and no amount of UI work fixes that. Measuring first would tell you whether this is a design question at all.
Key takeaways
- CQRS separates the model that enforces invariants on write from models shaped for reads.
- The benefit that scales is read models shaped for their queries, plus independent scaling and a clean write model.
- The dominant cost is user-visible eventual consistency — read-your-own-writes must be designed for, not discovered.
- It is a spectrum. Separate methods cost nothing; separate models over one store give most of the shape benefit with no lag; separate stores add asynchrony; event sourcing is a further, independent decision.
- CQRS and event sourcing are independent. Adopting both at once is a common and expensive mistake.
- Monitor projection lag and be able to rebuild any projection, or a stalled projection serves stale data silently.
- Before adopting, name the specific query that is a problem. Unnamed slow reads are usually an indexing issue.
Further reading
- Greg Young, "CQRS Documents" (2010) — the original articulation.
- Martin Fowler, "CQRS" (2011) — including the argument that most systems should not use it.
- Chris Richardson, Microservices Patterns (Manning, 2018) — CQRS and API composition compared.
- Vaughn Vernon, Implementing Domain-Driven Design (Addison-Wesley, 2013).
- Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017) — derived data and materialised views.
- Pat Helland, "Immutability Changes Everything" (CIDR, 2015).