Home › High-Level Design › CQRS
C

CQRS

Most teams reach for CQRS one layer too early, and interviewers love to catch that. CQRS is a sharp tool for one specific pain: when your reads and writes want completely different data shapes, different scaling curves, and different consistency guarantees. The moment you say "CQRS" in an interview, the good interviewer stops caring about the definition and starts asking what you gave up to get it. I have watched strong candidates lose a loop not because they did not know CQRS, but because they could not name the price.

High-Level DesignAdvanced11 min read

If you remember five things

The whole concept in one glance. Everything below defends these five calls.

  1. Split the models. Commands (writes) and queries (reads) stop sharing one model. The write side guards invariants; the read side is shaped exactly for how it is consumed.
  2. It is a spectrum, not a switch. CQRS can be two code paths on one database all the way up to split stores fed by a change stream. Do not confuse it with event sourcing or two databases.
  3. You pay in staleness. Reads lag writes. The whole design question is how much lag you tolerate and where you refuse to.
  4. Justify it with a ratio. Reach for it when reads dwarf writes (roughly 10:1 or more) and the read shape differs from the write schema, not because it sounds advanced.
  5. Projectors must be boring. Idempotent, lag-monitored, and replayable. That is the operational cost you take on, and the interviewer wants to hear you name it.

1The core idea#

Strip the jargon and CQRS (Command Query Responsibility Segregation) says one thing: stop forcing your writes and your reads to use the same model.

In plain CRUD, one model does everything.

  • The same Order object and orders table handle "place an order," "show my order history," and "show the ops dashboard."
  • That works fine until the jobs start fighting.
  • Writes want normalized tables and strong invariants. Reads want denormalized, pre-joined, pre-aggregated views.
  • At scale a six-way join on every request is the gap between a 20ms p99 and a 400ms p99, and the read path is the one your customers feel.

CQRS cuts the system into two halves:

  • The command side. Handles writes. A command is an intent: PlaceOrder, CancelReservation, ApplyDiscount. It validates business rules, enforces invariants, and changes state. Commands return success or failure, not data.
  • The query side. Handles reads. It serves one or more read models shaped exactly for how the UI or API consumes them. Queries never change state.

The mental model I give juniors: a busy restaurant.

  • The kitchen (commands) cares about correctness, order, and not burning anything.
  • The display board (queries) cares about being fast and easy to read.
  • You never make every customer walk into the kitchen to check on their food. You post a status board, and you accept it is a few seconds behind the stove.
  • That lag is the whole deal with CQRS, and it is the first thing a sharp interviewer presses on.
The one thing people get wrong

CQRS does not require two databases, does not require event sourcing, and does not require message queues. Those are common companions, not the definition. At its simplest, CQRS is two code paths against the same database, one tuned for writes and one for reads.

2How it works#

Here is the version you will usually draw on a whiteboard: a write store, a read store, and something that keeps the read side in sync.

The flow, step by step:

  • A client sends a command.
  • The command handler validates it and commits to the write store, the system of record.
  • That commit emits a change: an event, a change-data-capture row, or a message on a bus.
  • A projector (sometimes called a denormalizer) consumes the change and updates one or more read models.
  • Queries hit the read models directly and never touch the write store.
flowchart LR A[Client] --> B[Command API] A --> F[Query API] B --> C[Write Store] C --> D[Change Stream] D --> E[Projector] E --> G[(Read Model)] F --> G
The two halves meet only at the change stream. Writes flow left to right through the projector into the read model; reads go straight to the read model and never touch the write store. That clean wall is the whole point.

The sync in the middle is where the design decisions live. Three ways to wire it:

  • Synchronous, same transaction. Update the write table and the read view in one transaction. Simple and strongly consistent, but now write latency includes read-model maintenance and you lose the independent scaling that made CQRS attractive. Call it "CQRS lite."
  • Change data capture. Tail the write database's log (Postgres logical replication, MySQL binlog, DynamoDB streams) and project from there. No application code to emit events, and you catch every change, including ones that bypass your API.
  • Event-driven. The command side publishes domain events to Kafka or a similar bus, and projectors subscribe. This pairs naturally with event sourcing but does not require it.

The timing that trips people up:

sequenceDiagram participant U as User participant C as Command Side participant S as Change Stream participant R as Read Model U->>C: PlaceOrder C-->>U: 202 Accepted C->>S: order placed event S->>R: project order U->>R: GET my orders R-->>U: order not here yet
Watch the gap. The user got a 202, fired a read, and the projection had not landed yet. That window (often 50ms to a few seconds) is eventual consistency, and your product has to be designed for it.
Red flag that fails you here

Drawing the two boxes and treating the sync arrow as a detail. The arrow is the interview. If you cannot say which mechanism you picked (same transaction, CDC, or events) and why, you have shown the pattern without showing you have run it.

3A worked example with real numbers#

Theory is cheap. Sizing a concrete system is the move that separates a passing answer from a hand-wave in the loop.

The system: an e-commerce product catalog at a mid-size marketplace. Numbers I would put on the board:

  • Read traffic: 50,000 product-page views per second at peak, roughly 4.3 billion reads per day.
  • Write traffic: 500 price or inventory updates per second at peak, mostly from seller tools and inventory syncs, about 43 million writes per day.
  • Read-to-write ratio: 100 to 1.
  • A single product page needs data from 7 tables: product, pricing, inventory, seller, reviews-summary, shipping-options, and category-breadcrumb.
100:1
The ratio that justifies the whole splitReads outnumber writes 100 to 1, and each read needs a 7-table join the write schema cannot serve cheaply. That asymmetry is the entire reason to pay for CQRS here. No asymmetry, no CQRS.

In plain CRUD, each page view runs that 7-way join.

  • Say each join hits an index and lands in 40ms warm, 220ms cold.
  • At 50,000 reads per second, the join work alone would need north of 2,000 database cores served straight from the normalized write store.
  • That is absurd, and it couples read scaling to write scaling.

Now CQRS.

  • Keep the normalized write store: 7 tables, strong invariants (a seller cannot set negative inventory).
  • On every committed change, capture the row via Postgres logical replication.
  • Run a projector that writes one flat denormalized document per product into a read store (DynamoDB or a document cache).
  • The read store holds about 20 million product documents at roughly 4KB each: 80GB, trivially frontable with Redis.

The payoff in numbers:

  • A product-page read is now a single key lookup, GET product:{id}. p50 drops to 3ms, p99 to 12ms, versus the old 40ms to 220ms.
  • The read store scales on the read curve (shard by product id, add replicas). The write store stays small, seeing only 500 writes per second, which one primary handles comfortably.
  • Projection volume is 500 events per second, trivial for one Kafka partition set and a handful of projector workers.
Pause before you read on: a seller drops a price. The read model now lags. Where is that lag unacceptable, and what do you do there?
Reveal the answer
On a browse page a 1-second stale price is a non-event, so eventual consistency is fine. At checkout it is not: you cannot charge a customer a price they no longer agreed to. The fix is to re-read the authoritative write store at the moment of purchase, bypassing the read model entirely. Naming the one place you refuse to tolerate staleness is what makes the answer read as senior.

And the price, stated honestly:

  • When a seller changes a price, the read model lags. You measure that lag.
  • With logical replication plus a projector, the staleness budget is under 2 seconds at p99, under 500ms at p50.
  • For a catalog that is fine. A price 1 second stale on a browse page is a non-event.
  • At checkout, re-read the authoritative write store so the customer is never charged a stale price. That one sentence, naming where you refuse to tolerate staleness, is what reads as senior.

4Trade-offs#

This is the section interviews actually reward. CQRS buys you real things and charges you real things.

What you gain
The upside
Independent scaling
Read models, caches, and replicas scale on the read curve; the write side scales on its own.
Query-shaped reads
No runtime joins. A product page reads one document; a report is pre-aggregated.
Cleaner writes
The command side drops presentation concerns. Invariants live in one place.
Many views, one source
One order stream feeds a customer view, a fraud model, and a warehouse export.
What you pay
The downside
Eventual consistency
Reads lag writes by default. Read-your-own-writes is no longer free.
More moving parts
Projectors, a change stream, replay tooling, lag monitoring. More to break at 3am.
Duplicated data
The same fact lives in the write store and in N read models. Rebuilds and migrations get harder.
Harder debugging
"The dashboard is wrong" now means "which stage: write, event, projector, or read model?"

A quick comparison I use when deciding where on the spectrum to land:

Dimension Plain CRUD CQRS lite (one DB) Full CQRS (split stores)
Read latency ties to write schema tunable lowest, purpose-built
Consistency strong strong eventual
Operational cost low medium high
Write throughput ceiling shared with reads shared independent
Good fit when read and write shapes match one DB, reads need tuning read load dwarfs writes

My honest opinion after shipping a few of these:

  • Start at CRUD.
  • Move to CQRS lite (separate read and write code paths, maybe a read replica) when read queries start hurting.
  • Go full split-store only when you have a measured reason.
  • Jumping straight to event-sourced CQRS on a greenfield service is the most common way I have seen teams bury themselves in accidental complexity.

5Where it is used#

CQRS shows up anywhere the read and write worlds genuinely diverge.

  • E-commerce catalogs. Writes are rare (a seller updates a price), reads are enormous (millions of product-page views). The page reads a flat denormalized document built from a dozen upstream tables. The exact case sized above.
  • Order and payment systems. The write side enforces strong invariants (you cannot ship what is not paid). Separate read models serve the customer order view, the finance reconciliation report, and the ops dashboard, each totally different.
  • Social feeds and activity streams. Writing a post is one small event. Reading a feed is a heavily precomputed, fanned-out read model. The timeline you scroll is a read projection, not a live join across who-follows-whom.
  • Collaboration and audit-heavy tools. Paired with event sourcing, the command log becomes a full audit trail for free, and read models are just replays of that log into useful shapes.

When I reach for it in an interview answer: when I can point at a specific pressure.

  • Read-to-write ratio above roughly 10 to 1.
  • Read queries that need a shape the write schema cannot serve cheaply.
  • A need to feed several very different consumers from one source of truth.

If none of those are true, I say so, and I do not add CQRS just to sound advanced. That restraint scores points.

6Common pitfalls and gotchas#

Treating eventual consistency as a bug instead of a contract. The number one failure.

  • A user updates their profile, the API returns 200, they refresh, and the old value shows because the read model is 300ms behind.
  • Fix one: read-your-own-writes, routing a user's own reads to the write store briefly.
  • Fix two: return the new state in the command response so the UI updates optimistically without re-reading.
  • Decide this on purpose. Do not discover it in production.

Unbounded projection lag. Projectors fall behind during spikes or after a deploy.

  • Nobody notices until the data is minutes stale.
  • Monitor lag as a first-class metric and alert on it.
  • Treat projection lag like replication lag, and set the alert at the staleness budget you promised (2 seconds in the catalog example).

Non-idempotent projectors. Event delivery is usually at-least-once.

  • Your projector will see the same event twice.
  • If "add $10" runs twice, the balance is wrong.
  • Projections must be idempotent, keyed on an event ID or version, so a replay is safe.

No replay story. You will eventually need to rebuild a read model from scratch.

  • A projector bug corrupted it, or you want a new view.
  • If you cannot replay the source of truth into a fresh read model, you are stuck.
  • Design the rebuild path before you need it.

Leaking writes through the read side. Someone adds a tiny update inside a query handler "just this once."

  • Now the read model is also a write path, and your consistency guarantees are gone.
  • Keep the wall clean.

Overusing it. CQRS on a service with 50 writes a day and matching read shapes is pure overhead.

  • The gotcha is cultural: resist adding it because it is in the architecture deck.

7How it shows up in interviews#

Interviewers rarely ask "what is CQRS." They hand you a design problem (a ticketing system, a feed, an order service) and watch whether you introduce CQRS at the right moment for the right reason.

What they probe:

  • Do you justify it with numbers? Weak: "We'll use CQRS for scale." Strong: "Reads are about 50x writes and the product page needs a denormalized view, so I split the read model and feed it via change data capture, accepting roughly sub-second staleness."
  • How do you handle the consistency gap? They want you to name read-your-own-writes and propose a concrete fix, not wave it away.
  • How do projections stay correct? Idempotency, ordering, lag monitoring, and replay. Mentioning replay unprompted signals you have operated one of these.
  • Do you know when NOT to use it? "Here CRUD is enough, the read and write shapes match and traffic is low" is often the strongest answer in the room.
  • Can you separate CQRS from event sourcing? Many candidates fuse them. Saying "I'm using CQRS here but a simple projection off CDC, not full event sourcing, because I don't need the audit log" shows real depth.

Here is the contrast I want you to hear in your own answer, using the catalog from section 3.

Candidate (weak): Product pages are slow, so I'll add CQRS. We'll have a write database and a read database, and the read one will be fast. That handles scale.

Candidate (strong): Reads are 50,000 per second against 500 writes, a 100 to 1 ratio, and a page needs a 7-table join. I keep the normalized Postgres write store for invariants, capture changes with logical replication, and project a flat per-product document into a read store fronted by Redis. That turns a 40ms join into a 3ms key lookup. I accept eventual consistency with a p99 staleness budget under 2 seconds, which I monitor and alert on. Browse pages tolerate that, but at checkout I re-read the write store so nobody is charged a stale price. Projectors are idempotent keyed on event version, and I have a replay path to rebuild the read model from the write log if a projection bug corrupts it.

The weak answer names the pattern and nothing else: no numbers, no sync mechanism, no staleness budget, no mention of what breaks. The strong answer sizes every claim, names every trade, and has the failure modes already handled. That is the difference between someone who read about CQRS and someone who has been paged by it.

A weak answer bolts CQRS onto everything and hand-waves the lag. A strong answer treats it as a deliberate trade: name the pressure, pick the sync, state the staleness budget in real units, and show you can monitor and rebuild it.

Follow-ups the interviewer will ask (and your answers)
"A user updates their profile and immediately sees the old value. How do you fix that?"
That is the read-your-own-writes gap, baked into eventual consistency. Two concrete fixes: route that user's own reads to the write store for a short window after their write, or return the new state in the command response so the UI updates optimistically without re-reading. I decide which on purpose rather than discovering it in production.
"Your projector sees the same event twice. What happens to the data?"
Nothing bad, if I built it right. Event delivery is at-least-once, so I make projections idempotent, keyed on an event ID or version. A replayed event is a no-op. If I naively ran 'add $10' per event, a retry would corrupt the balance, so the projection has to be keyed, not incremental.
"A projection bug corrupted the read model. How do you recover?"
I replay. The write store (or event log) is the source of truth, so I rebuild a fresh read model by re-projecting from it, then cut over. This is why I design the replay path before I need it, and why idempotent projectors matter: a rebuild re-runs every event safely.
"Is this CQRS or event sourcing? Aren't they the same?"
No. CQRS splits the read and write models. Event sourcing stores state as an append-only log of events instead of current rows. They pair well (the event log makes a natural projection source) but are independent. Here I use CQRS with a simple projection off change data capture, not full event sourcing, because I do not need the audit log.
"How do you keep the read model from falling minutes behind without anyone noticing?"
I treat projection lag like replication lag: a first-class metric with an alert set at the staleness budget I promised, so 2 seconds in the catalog case. If projectors fall behind during a spike or after a deploy, I get paged before the data is stale enough to matter, and I can scale projector workers or repartition the stream.

What you can now explain in the room

  • Why forcing reads and writes through one model breaks down, and what the split actually buys.
  • That CQRS is a spectrum from two code paths on one DB up to split stores, and that it is not event sourcing.
  • How to size the decision: a 100:1 read-to-write ratio and a 7-table join turning a 40ms read into a 3ms lookup.
  • Where you tolerate staleness (browse) and where you refuse it (checkout re-reads the write store).
  • The three sync mechanisms (same transaction, CDC, event bus) and when each fits.
  • Why projectors must be idempotent, lag-monitored, and replayable, and how you recover a corrupted read model.

For a full worked example, see the System Design Question walkthrough on designing a high-traffic e-commerce product catalog, where the read-write split drives the whole design.

Get a prep plan built around your CQRS interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your CQRS prep plan is on the way.