Distributed systems
CQRS: interview questions and how to answer them
CQRS uses different models for writing and reading, so each can be shaped for its own job instead of compromising on one.
Written and reviewed by Sahil Srivastav
What it actually is
CQRS — Command Query Responsibility Segregation — means the model you write through and the model you read from are different. Writes go through a model organised around invariants and business rules; reads come from one or more models shaped exactly like the queries they serve.
The motivation is that these two jobs genuinely want different shapes. A write model wants normalisation, constraints and small aggregates with clear boundaries, because its job is to reject invalid state. A read model wants denormalisation and precomputation, because its job is to answer a specific question in one access. A single model serving both is a compromise, and on read-heavy systems it is usually the reads that suffer.
The term covers a wide range of commitment, which is where confusion comes from. At its lightest, it is separate classes for commands and queries hitting the same database — a code-organisation choice with essentially no operational cost. At its heaviest, it is separate write and read databases synchronised asynchronously, which is a distributed system with all that implies. People argue about CQRS while meaning different points on that scale.
Why it matters in production
Because the version worth most attention is the one with the real cost: once the read model is updated asynchronously, it is eventually consistent, and that leaks directly into the user interface. A user saves a change and the list they return to does not show it yet. This is not a bug to fix later — it is the architecture working as designed, and it has to be handled deliberately in the UI.
And because that cost is frequently paid for no reason. Most systems reaching for full CQRS would have been served by a materialized view, a cache, or an index. The question that separates experience from reading is not "how does CQRS work" but "what made the single model insufficient", and a candidate who cannot answer the second has usually adopted the pattern prematurely.
How it works
Commands reject, queries never mutate
A command expresses intent, is validated against the write model's invariants, and may be refused. A query returns data and changes nothing. Enforcing that split rigorously is most of the value even without any infrastructure change, because it makes the rules live in one place rather than being scattered through read paths.
The read model is derived and disposable
Projections are built from the write side — by events, by CDC, or by a synchronous update — and shaped for specific screens. Being derived means being rebuildable: a new query becomes a new projection rather than a schema migration, and a corrupted projection is dropped and regenerated rather than repaired.
Asynchronous propagation makes it eventually consistent
The moment the read model updates out-of-band, there is a window where a write is committed and not yet visible. The window is usually milliseconds and is unbounded under failure. Any product decision made from a read — "is this already taken?" — must account for it, which is why validation stays on the write side.
Handling the lag in the interface
Three workable approaches. Return the resulting state from the command so the client renders it directly without re-reading. Apply the change optimistically and reconcile when the projection catches up. Or have the client poll until the projection reflects the write, which is honest but slow. Pretending the lag does not exist produces the "my change disappeared" bug report.
Separation does not require two databases
Separate models in one database is a legitimate and common implementation — a normalised write schema plus materialized views or denormalized read tables, updated transactionally. This keeps strong consistency and removes the hardest problem, and it is the version most teams should start with.
Implementing it
Start with the lightweight version: separate command and query paths in code, one database, no async. It captures most of the clarity benefit at no operational cost, and it is reversible.
Escalate to a separate read store only when you can name the specific pressure — read volume the write database cannot serve, or a query shape it fundamentally cannot answer well, such as full-text or geospatial.
Decide the staleness handling before shipping, not after the first complaint. Returning the new state from the command is usually the cheapest effective answer.
Keep projections genuinely rebuildable and exercise that path regularly. A projection nobody can regenerate has become a second source of truth, which defeats the model.
// Command: expresses intent, may be rejected, returns the resulting state so
// the client does not have to read a projection that has not caught up yet.
const result = await placeOrder({ customerId, items, idempotencyKey });
render(result.order); // no re-read, no lag visible to the user
// Query: served by a projection shaped for this screen. One access,
// no joins, denormalised deliberately.
const rows = await db.query(
'SELECT order_id, customer_name, item_count, total_minor, status\n' +
' FROM order_list_view WHERE customer_id = $1\n' +
' ORDER BY placed_at DESC LIMIT 20', [customerId]);Interview questions and how to answer them
What problem does CQRS solve?
That a single model serving both writes and reads is a compromise. Writes want normalisation and constraints to reject invalid state; reads want denormalisation and precomputation to answer in one access. Separating them lets each be shaped for its job. The honest caveat is that most systems do not have this problem badly enough to justify the heavyweight version.
What breaks when the read model is updated asynchronously?
Read-your-own-writes. A user saves and the list does not show it yet, because the projection has not caught up. The window is usually milliseconds and unbounded under failure. Fixes: return the new state from the command, apply optimistically and reconcile, or poll until visible. What does not work is assuming the propagation is fast enough.
Where do validation and business rules live?
On the write side, always. The read model may be stale, so a rule enforced against it can be decided on outdated information — "this username is free" from a projection that has not seen the last two registrations. The write model, with its constraints, is the only place a rule can actually be guaranteed.
Does CQRS require event sourcing?
No. They are frequently paired because event sourcing needs projections for reads, which is CQRS. But CQRS works perfectly well over a conventional database — a normalised write schema plus materialized views is CQRS with strong consistency and none of the event-sourcing costs.
When is it the wrong choice?
When the read and write shapes are similar and the load is modest, which describes most CRUD applications. You pay projection maintenance, eventual consistency and more moving parts for a flexibility the product never uses. A materialized view or an index usually delivers the actual benefit people were reaching for.
Answers that lose the round
- Adopting the heavyweight version without being able to name what the single model could not do
- Validating business rules against the read model, which may be stale
- Treating read-model lag as a bug to be fixed rather than a property to be designed around
- Building projections that cannot be rebuilt, making them a second source of truth
- Assuming CQRS requires event sourcing
- One projection per entity rather than per query, which recreates the compromise it was meant to remove
- No idempotency in projection updates, so a redelivery double-applies
FAQ
Is a materialized view CQRS?
It is the simplest honest implementation of the read side: a precomputed, denormalised model maintained from the write model. If you have a normalised schema and query through views shaped for screens, you are doing lightweight CQRS already — which is a useful thing to recognise before adopting anything heavier.
How many read models should there be?
One per query shape that genuinely needs its own, not one per entity. The benefit comes from a model matching a specific screen or report; a generic per-entity read model rebuilds the same compromise the pattern was meant to remove.
How do you keep projections in sync?
Synchronously in the same transaction for strong consistency and simplicity, or asynchronously via events or CDC for scale and decoupling. Async needs idempotent application and a reconciliation job, because a dropped or reordered message otherwise leaves a divergence nothing detects.
Does CQRS mean separate services?
No. It is a model-separation pattern and lives comfortably inside one service and one database. Splitting into separate deployable services is an orthogonal decision that adds network failure and distributed-consistency concerns CQRS does not require.