Architecture and Scale

CQRS

Command Query Responsibility Segregation splits reads from writes into separate models. It unlocks independent scaling and optimization but adds complexity. A common advanced interview topic.


The Concept Explained

Most systems use one model for everything. The same entity classes and the same tables serve the code that changes data and the code that reads it.

That works until the two uses start pulling in opposite directions, which they do surprisingly often.

Writes want normalization. One fact in one place, so updates touch a single row and invariants are enforceable. Normalized schemas are correct and they make reads expensive, because assembling anything useful means joining across many tables.

Reads want denormalization. The data shaped exactly as the screen needs it, so a query is a single lookup. That means duplicating data across read structures, which is precisely what makes writes complicated.

One model cannot be optimal for both. CQRS accepts that and stops trying: separate the command side, which changes state, from the query side, which reads it, and let each be designed for its own job.

KEY CONCEPT

CQRS is not about splitting code into command and query classes, which is just tidy design. It is about having genuinely different models, and usually different data stores, for writing and reading, connected by a projection that keeps the read side updated. The moment those are separate stores, the read side is behind the write side, and everything difficult about CQRS follows from that lag.


How It Works

A command arrives and is handled by the write model, which enforces invariants and persists the change in a normalized, correctness-oriented form. The change then propagates to one or more read models, each shaped for a specific query pattern. Queries hit those read models directly, never the write model.

A Write Travelling to the Read Models

Click each step to explore

What the Split Buys

Independent scaling. Most systems are read-dominated by a wide margin. Separate models means adding read capacity without touching the write path, and the read stores can be replicated freely because they are derived rather than authoritative.

Genuinely different technology per side. A relational store for writes where transactions and constraints matter, a search index for text queries, a cache for hot lookups, a columnar store for analytics. Each read model uses whatever fits its query pattern.

No contention between the two. Heavy analytical queries stop competing with transactional writes for locks and buffers.

Simpler models on both sides. The write model carries only what is needed to validate and change state. The read models carry only what their screens display, with no attempt to serve every use.

What It Costs

Eventual consistency, and this is the big one. A user performs an action and immediately reads back state that has not yet been projected. Their change appears not to have happened. This is the read-your-writes problem, it is the most common complaint about CQRS in production, and it must be designed for rather than discovered.

The usual mitigations: return the result directly from the command so the client does not need to re-read, have the client hold the expected new state optimistically, or route a user's reads to a synchronously-updated model briefly after they write.

Projections are code that can fail. Each one is a consumer that can lag, crash, or contain a bug. Failed projections mean a read model diverging from truth while continuing to serve queries confidently. You need monitoring on projection lag and a rebuild path, and the rebuild path needs to exist before you need it.

More moving parts. Multiple stores, projection processes, and a messaging path between them. All must be deployed, monitored, and reasoned about.

Debugging spans the pipeline. A wrong read value could be a bad command, a bad write, a projection bug, or projection lag. Four candidate causes rather than one.

WARNING

CQRS is frequently adopted for systems that never needed it, and the tell is that the read and write models end up nearly identical. If your projection is copying rows into a table with the same shape, you have taken on eventual consistency, projection failure modes, and multiple stores in exchange for nothing. The pattern earns its cost only when the two sides genuinely want different shapes.

The Relationship to Event Sourcing

The two are frequently mentioned together and they are separate decisions.

CQRS says reads and writes use different models. Event sourcing, next lesson, says the write model stores events rather than current state. You can have either without the other.

They pair naturally because event sourcing produces a stream of changes, which is exactly what projections need to consume, and because an event-sourced write store is poor at queries, which makes a separate read model close to mandatory. That mutual fit is why they are often introduced together, and it is also why people mistakenly believe one requires the other.


System Design Implications

Apply it where the read and write patterns genuinely differ. An e-commerce catalogue written by a handful of merchandisers and read by millions with faceted search. An order system where writes are transactional and reads feed dashboards. A collaborative tool where writes are fine-grained and reads want assembled documents.

Apply it per bounded context, not across the whole system. The strongest designs use CQRS for the few areas with a real asymmetry and keep everything else on a single model. Applying it globally is how the complexity becomes overwhelming for little return.

Design the staleness experience explicitly. Decide per screen how much lag is acceptable and what the user sees meanwhile. Returning the command's result directly is the simplest fix and covers most cases.

Make projections rebuildable from the start. A projection bug means the read model is wrong and must be regenerated. If the write store holds enough history to rebuild, this is routine. If it only holds current state, some corrections are impossible, which is one of the arguments for event sourcing alongside.

Use an outbox for the change notification. The write commit and the change publication must be atomic, or read models will silently miss updates. This is the dual-write problem and the outbox is the standard fix.

Monitor projection lag as a first-class metric. It is the health signal for the whole read side, and it is the leading indicator of users seeing stale data.

PRO TIP

Answer the "when is it overkill" half concretely: if the read and write models would look substantially the same, CQRS buys nothing and costs eventual consistency plus projection failure modes. The justification is a genuine asymmetry, either in shape, where reads need denormalized data that writes cannot maintain cheaply, or in volume, where reads dwarf writes enough that independent scaling matters.


Tradeoffs and Decision Framework

DimensionSingle modelCQRS
ConsistencyImmediateEventual on the read side
Read optimizationConstrained by write schemaFree, per query pattern
ScalingTogetherIndependent
Number of storesOneTwo or more
Failure modesStore failureStore failure plus projection lag and bugs
Read-your-writesAutomaticMust be designed
Debugging a wrong valueOne placeCommand, write, projection, or lag
SuitsBalanced load, similar shapesRead-heavy, divergent shapes

The framework in four questions. Do reads and writes genuinely want different shapes, since that is the core justification? Is the read-to-write ratio lopsided enough that independent scaling matters? Can the business tolerate a short staleness window on the affected screens? And is there operational capacity to run projections, monitor their lag, and rebuild them when they break?

Four yeses make a strong case, for that bounded context. Anything less and a single model with read replicas and caching solves most of the problem at a fraction of the cost, and proposing that in an interview shows better judgement than reaching for the more sophisticated pattern.


Common Mistakes

Applying it system-wide. It belongs in the specific contexts with a real asymmetry, not as a global architectural style.

Read and write models that end up identical. All the cost, none of the benefit, and a clear sign the pattern was not needed.

Not designing for read-your-writes. Users seeing their own change missing is the complaint that actually arrives, and it is entirely predictable.

Dual-writing to both stores. Writing to the write model and the read model separately loses updates on failure. Use an outbox and a projection.

Projections that cannot be rebuilt. A projection bug requires regeneration, and without sufficient history in the write store some corrections are impossible.

Not monitoring projection lag. It is the health metric for the entire read side and the earliest signal that users are seeing stale data.

Confusing it with event sourcing. Separate decisions that pair well. Neither requires the other.

Treating it as just splitting classes. Separating command and query methods is tidy code. CQRS means different models and usually different stores.


INTERVIEW QUESTION

What problem does CQRS solve by separating reads from writes? When is the added complexity worth it, and when is it overkill?