08. CQRS — split commands and queries when one model starts hurting both¶
⏱️ Estimated time: 15 min | Level: intermediate
ELI5 callback: The town crier pins one notice on the bulletin board. The board rules decide delivery, and the town directory helps readers find it.
Why one model often hurts both reads and writes¶
One write model often becomes too strict for reporting needs. One read model often becomes too loose for invariants. CQRS separates commands from queries on purpose. See. That separation can stay logical first and physical later. You do not need two databases on day one. You do need clarity about who changes state and who reads it. Diagram:
+-----------+ +-----------+
| commands | ----------> | write side|
+-----------+ +-----------+
\ /
\ /
+------ queries ------+
- CreateOrder changes state and must respect stock rules.
- ListMyOrders only reads data and wants a quick answer.
- One table shape rarely serves both perfectly.
- CQRS gives each concern its own optimization path.
- Complexity should rise only when pain is real.
- Writes protect invariants.
- Reads optimize shape and speed.
- Separation can be in code, storage, or both.
- Start small and split further only when needed. One size fits all usually means one size annoys all.
The command side protects truth before convenience¶
Commands represent intent and deserve validation near the aggregate. The write side protects invariants before any state change. So what to do? Let one town crier accept the command and enforce business rules. If it succeeds, emit one committed notice for downstream readers. The write model may stay normalized and strict. It should optimize correctness before convenience. Diagram:
+-----------+ validate +------------+
| command | -----------> | aggregate |
+-----------+ +------------+
| |
v v
reject bad intent commit business fact
- ReserveSeat checks capacity before confirming.
- ChargeWallet checks balance before debit.
- The aggregate owns those decisions tightly.
- Successful commands create durable events.
- Failed commands return errors, not half-writes.
- Keep command handlers small and explicit.
- Put invariants where state changes.
- Avoid query-style shortcuts inside write paths.
- Measure command latency separately from read latency. Strong writes make the rest of the system calmer.
The query side shapes data for questions people actually ask¶
Query models answer questions, not moral philosophy. Denormalized tables, caches, and search indexes are fair game here. A practical town directory helps callers reach the correct read model. Read endpoints can be shaped for screens, reports, or APIs. They may duplicate data because duplication buys speed. Now watch. This is acceptable when rebuild paths stay clear. Diagram:
+-------------+ build +----------------+
| events/data | ---------> | read model |
+-------------+ +----------------+
| |
v v
denormalize rows answer fast queries
- A dashboard view precomputes daily revenue totals.
- A profile page view joins customer and order summaries.
- A search index stores text in query-friendly form.
- None of these shapes need to be the write model.
- They exist to answer specific questions quickly.
- Read models can be disposable.
- Rebuild paths matter more than elegant schemas.
- Duplicate data is acceptable when ownership stays clear.
- Tailor indexes to concrete query patterns. Users pay for fast answers, not for your database purity.
Events keep read models fresh, but not instantly perfect¶
The write side and read side talk through events. The bulletin board carries changes to every interested projector. Projectors consume, transform, and store query-ready data. Lag appears because propagation is asynchronous. Users might see stale data for a short window. Design product copy and refresh behavior around that truth. Simple, no? Diagram:
+-----------+ publish +-------------+ update +-----------+
| write side | ---------> | projector | ---------> | read side |
+-----------+ +-------------+ +-----------+
commit async step query now
freshness depends on projector speed and backlog size
- OrderConfirmed lands on the stream.
- Revenue projector updates totals a second later.
- Profile projector updates customer history separately.
- Search index may lag even longer during backfills.
- Product wording should admit that tiny window honestly.
- Eventual consistency must be visible and measured.
- Replayable projectors reduce fear during failures.
- Offset tracking is part of operational safety.
- Duplicate event handling matters for every projector. Fresh enough beats perfectly synchronized when the trade-off is worth it.
CQRS adds power only when the trade-offs are accepted clearly¶
CQRS adds moving parts, so it must earn its keep. Consistency windows, replay steps, and backfills become operational work. Lag alarms and rebuild steps are part of the board rules. Do not split models just because the term sounds senior. Use it when write invariants and read shapes truly diverge. Avoid it for small CRUD apps with ordinary traffic. The pattern solves tension, not boredom. Diagram:
+-------------+ trade-off +----------------+
| simpler app | <-----------> | CQRS system |
+-------------+ +----------------+
lower ops work richer tuning
fewer moving parts more async parts
less flexibility better specialization
- List your top five queries by cost and frequency.
- List the invariants that the write side must protect.
- If both lists fight each other, CQRS may help.
- If not, stay simple and move on.
- Maturity means saying no when the pattern is unnecessary.
- Complexity should buy a measurable benefit.
- Operational playbooks matter as much as architecture diagrams.
- Read and write ownership should stay unambiguous.
- Small teams should price the maintenance burden honestly. Use CQRS because the system asks for it, not because slides did.
Where this lives in the wild¶
- Microsoft and Azure guidance, where CQRS appears in many reference architectures.
- EventStoreDB projections feeding specialized read models from committed streams.
- LinkedIn-style feed and profile systems serving different read shapes at scale.
- Netflix recommendation and playback views tuned for query-heavy workloads.
- Search-backed commerce catalogs combining strict writes with flexible reads.
Pause and recall¶
- Why can logical CQRS exist before separate databases exist?
- What belongs on the command side that should not leak to queries?
- Why are stale read models acceptable only with clear rebuild paths?
- When does CQRS become needless ceremony?
Interview Q&A¶
Q: What does CQRS actually separate? A: It separates the models and handling paths for state-changing commands and state-reading queries. Common wrong answer to avoid: "It only means using two databases for every service."
Q: Why are read models often denormalized? A: Because they are optimized for answering concrete queries quickly rather than protecting write invariants. Common wrong answer to avoid: "Denormalization is a mistake that CQRS forces you to accept."
Q: What new operational burden comes with CQRS? A: Projection lag, backfills, replay tooling, and consistency communication all become real responsibilities. Common wrong answer to avoid: "Once you split reads and writes, operations become easier automatically."
Q: When should you avoid CQRS? A: When one straightforward model already handles your invariants and queries without painful trade-offs. Common wrong answer to avoid: "Always add CQRS early so the architecture looks future-proof."
Apply now (5 min)¶
Take one feature, like checkout or ticket booking. Write two commands and the invariants each must protect. Then list three high-volume queries the product needs. Sketch one write model and two separate read models. Mark the event that refreshes each read model. Note the maximum lag your users can tolerate. Simple, no? The split should now feel concrete.
Bridge. CQRS splits read/write models. But how do services FIND each other in a distributed town? → 09