Module 29 / 46 · Phase F — Event-Driven at Scale · 40 min

CQRS.

Reads and writes have different shapes. One model serving both is a compromise neither side wins. The pattern that lets your write side enforce complex invariants while your read side stays brutally simple — and the asymmetry stops being a problem.

// What you'll know by the end

  • Why one model can't optimize for both
  • The command/query architectural split
  • CQRS with and without event sourcing
  • When the complexity pays off (and when it doesn't)
§ 01 — One model can't serve both

Reads outnumber
writes 100 to 1.

Look at almost any production system. The numbers are stark: a typical e-commerce site might serve 5,000 page views per second against 50 orders per second. A social platform serves billions of feed reads against millions of posts. Even systems we think of as "write-heavy" are usually read-heavy when measured honestly. And yet, the default architecture treats both as equal. One model, one set of tables, one path through the code. The writes and the reads share a database optimized for neither.

// WRITES AND READS DON'T LOOK ALIKE
WRITE · "place order"
BEGIN; INSERT...; UPDATE...; UPDATE...; COMMIT;
Touches 3–5 tables under a transaction. Must enforce invariants (sufficient stock, valid customer). The model needs to be normalized to prevent inconsistency. Slow but rare.
READ · "show order details"
SELECT ... FROM orders JOIN customers JOIN line_items JOIN products ...
Joins across 4–6 tables. Wants every field on screen in one query. The normalized write model is the wrong shape: every read pays for joins it didn't choose to need.
READ · "customer dashboard"
SELECT COUNT, SUM, GROUP BY ...
Aggregations across thousands of rows. Pre-computed would be instant. Live-computed melts the database. The normalized write model makes this query expensive every single time.
READ · "search products"
WHERE name LIKE '%...' OR description LIKE '%...'
Full-text matching wants Elasticsearch shape, not relational shape. The write model has no inverted index. The right tool is a different storage engine entirely.

The standard response to this asymmetry is to denormalize a little: cache here, materialized view there, read replica somewhere else. Each individual hack is reasonable. But after enough of them, the system has become a tangle of "main DB + 4 caches + 2 read replicas + an Elasticsearch + a Redis + that one Postgres view nobody dares to refresh." The accidental architecture is CQRS — just badly: there are clearly different read paths than write paths, but no one drew the line on purpose. CQRS is the same instinct made deliberate.

The pattern itself is small. Commands (intent-to-change) and queries (intent-to-know) flow through different models, different code paths, and often different storage. The write side is normalized, transactional, and enforces business rules. The read side is denormalized, eventually consistent, and shaped for the queries that actually run. One side does the changing; the other does the explaining; neither is trying to do both. That's it. The rest of the pattern is just the consequences of taking that split seriously.

One model for changes. A different model for explanations. The asymmetry was always there — CQRS just stops pretending.
§ 02 — The split

Commands change.
Queries describe.

The terminology comes from Bertrand Meyer's command-query separation principle from 1988 — any method should either change state OR return information, never both. Greg Young (the same engineer who named event sourcing) generalized this to entire architectures in the late 2000s. Commands are imperative requests to change state: "place this order", "deposit $500", "update this profile." They mutate. They can be rejected. Queries are requests for information: "show me my balance", "list my orders", "what's the top product?" They don't mutate. They can't fail in a business-rule sense. Different shapes, different needs.

// COMMANDS AND QUERIES, SIDE BY SIDE

// COMMAND SIDE · WRITE
Commands
"The world should change. Validate, then apply."

Receives imperative requests from clients. Loads the relevant aggregate (M.28), validates the command against current state and invariants, then applies the change. Optimized for consistency — every business rule lives here.

Verb tenseimperative: "DepositMoney"
Can failyes — invariant violations
Storage shapenormalized, transactional
Frequencylow (writes < reads)
Latency budgetuser wait time (~hundreds of ms ok)
Optimized forcorrectness
// QUERY SIDE · READ
Queries
"The world is asked about. Look up and return."

Receives information requests from clients. Hits a read model that's pre-shaped for the question — denormalized, indexed for the specific access pattern. Optimized for speed and the actual queries that run, not the abstract domain model.

Verb tenseinterrogative: "GetBalance"
Can failno — only "not found" or "no data"
Storage shapedenormalized, query-shaped
Frequencyhigh (reads >> writes)
Latency budgetpage render time (~tens of ms target)
Optimized forthroughput

The two sides communicate asynchronously. The command side accepts a command, applies it, and emits one or more events (or change notifications). The read side subscribes to those notifications and updates its own denormalized views. There's no shared database between them — they can use different storage engines entirely. Write side might be PostgreSQL with strict transactions; read side might be a combination of Redis (counters), Elasticsearch (search), and DynamoDB (key lookups), each shaped for what it serves.

The cost is real and worth naming: eventual consistency between writes and reads. A user places an order; the write side persists it; the read side updates 200ms later. Until then, if the user hits "view orders", the new order isn't there. Some UIs handle this gracefully (optimistic UI updates, "your order is being processed" messaging); some don't. CQRS demands that you think about this from day one — not as a bug to be fixed, but as a property of the architecture you've chosen.

Commands ask the system to change. Queries ask it to explain. Treating them as the same operation is the original sin of CRUD.
§ 03 — CQRS with and without event sourcing

Three architectures.
One idea.

The biggest source of confusion in the literature is that CQRS and event sourcing are independent patterns that almost always get discussed together. You can have either one without the other. The combination ("CQRS+ES") is so common that many tutorials conflate them — but they solve different problems. Knowing the three shapes — CQRS alone, ES alone, and the combo — is what separates "I've read about CQRS" from "I can choose between them."

// THREE WAYS THE PATTERNS COMBINE (OR DON'T)

CQRS alone// no event sourcing

Write side uses traditional state-based storage (UPDATE rows in tables). After every write, the system emits a change notification (or polls via change-data-capture) so the read side can update its denormalized views. Simpler than ES — no event store, no replay, no schema-evolution headaches. You still get the read/write separation, denormalized query models, and independent scaling. The right starting point when you need CQRS's benefits but don't need full history.

ES alone// no CQRS

Write side stores events as the source of truth (M.28). Reads happen by replaying events against the same model the writer used — usually with snapshots for performance. You get audit, time-travel, and replay, but reads and writes still share the model. Reasonable for small, balanced workloads; runs out of steam when reads grow much faster than writes or when read-side queries diverge significantly from the write-side aggregate shape.

CQRS + ES// the canonical combo

Write side stores events. Read side maintains multiple denormalized projections, each consuming the same event stream but shaped for a different query (current balance, transaction history, monthly stats, etc.). Each pattern reinforces the other: ES gives CQRS a natural change feed (the event log); CQRS gives ES a way to handle read scale without bloating aggregates. Powers banking, large e-commerce, and most "serious" event-driven systems. The complexity peak — also the capability peak.

A useful mental decision tree: does history matter as data? If yes (audit, replay, time-travel), event sourcing is on the table. Do reads and writes have very different shapes or scale? If yes, CQRS is on the table. Both yes → CQRS+ES. Only the second → CQRS alone. Only the first → ES alone. Neither → CRUD is fine; don't overbuild. The patterns aren't aspirational — they're tools that fit specific shapes. Picking them when the shape doesn't fit is how engineers earn the reputation that "all our microservices made things worse."

ES is a write-side choice. CQRS is an architecture-level choice. They marry well — but neither needs the other.
§ 04 — CRUD vs CQRS · side-by-side comparator

Three scenarios.
Two architectures.

Pick a scenario below. Watch the same business request flow through both a traditional CRUD architecture and a CQRS one. The first scenario is roughly a tie; the second is where CQRS earns its complexity; the third shows the architectural superpower CRUD can never match. Side-by-side is the only honest way to compare them.

CQRS.SIM // m.29 lab
// CRUD ARCHITECTURE monolithic
Loading...
// CQRS ARCHITECTURE split + async
Loading...
// HEAD-TO-HEAD VERDICT
Loading...
CRUD
...
CQRS
...
§ 05 — When CQRS earns its complexity (and when it doesn't)

A real cost.
An uneven payoff.

CQRS doubles the number of architectural components you maintain. Two models, two storage layers, an async sync mechanism between them, and eventual consistency that has to be explained to product managers. If the workload doesn't need what CQRS provides, you've taken on all that operational baggage for nothing. Greg Young — the person who named the pattern — has spent fifteen years warning teams about applying it where it doesn't fit. The most common production failure mode is "CQRS cargo-culted into a small CRUD app."

// WHERE CQRS PAYS OFF · WHERE IT DOESN'T

// GOOD FIT
Reach for CQRS when:
  • Reads vastly outnumber writes — and the read shape diverges from the write shape (most e-commerce, social, banking)
  • Multiple read models from one write — same order data needed as: order detail, customer history, sales dashboard, fraud screening
  • Different scale needs per side — you want to scale the read side independently (10× read replicas, single write primary)
  • Different storage engines fit each side — write side wants PostgreSQL transactions; read side wants Elasticsearch + Redis + materialized views
  • Complex domain on the write side — heavy invariant enforcement (banking, healthcare, regulated domains)
  • You're already event-sourcing — the event log naturally feeds projections; CQRS comes almost for free
// POOR FIT
Avoid CQRS when:
  • Simple CRUD app — admin panels, internal tools, small SaaS dashboards. The asymmetry is small; the doubling-cost isn't justified.
  • Read and write models look identical — if your reads are mostly SELECT * FROM users WHERE id = ?, denormalization buys nothing
  • You need strong read-your-writes — UX where "I just did X, now show me X" must work without delay (chat messages, real-time collaboration). Eventual consistency hurts here.
  • Small team, early stage — the operational complexity of two pipelines slows you down more than the architectural cleanliness helps. Ship CRUD first; refactor to CQRS when you actually need it.
  • Single storage engine fits both sides well — sometimes Postgres really is enough for everything
  • Reads and writes have similar scale — if your "reads" are 20 dashboards used by your ops team, you don't need a separate read side

A common evolution path that works well in practice: start with CRUD. Add caching and read replicas as needed. Notice when you have three caches that all serve the same underlying read pattern. That's the signal. The accidental architecture has become CQRS-shaped; making it deliberate now lets you simplify what was previously a patchwork. The version of this advice that actually applies: don't start with CQRS — but recognize when your CRUD architecture is asking you to.

One specific subset where CQRS is almost always wrong: simple-write systems with heavy domain logic on reads. If your business rules live in the queries (e.g., "show me customers who bought X and might also like Y"), splitting commands and queries doesn't help — the complexity is on the read side, where CQRS gives you nothing. The right answer for read-heavy intelligence systems is usually a data warehouse + dbt, not CQRS. Pattern-fit matters more than pattern-prestige.

CQRS is most valuable where reads and writes diverge most. Where they don't, it's just two databases pretending to be one.
§ 06 — Eight words for the CQRS conversation

Vocabulary,
for the split.

The terms every CQRS doc, every Greg Young talk, every "I refactored our monolith to CQRS" blog post assumes you already know.

Command
/kəˈmænd/
An imperative request to change state (PlaceOrder, UpdateProfile). Routed to the write side; can be rejected if it violates an invariant. Always written in the present-tense imperative.
Query
/ˈkwɪə.ri/
A request for information (GetOrderDetails, ListCustomerOrders). Routed to the read side; never mutates. Returns data shaped exactly for the asker.
Write Model
/raɪt ˈmɒd.əl/
The normalized, transactional data model the command side uses to enforce business rules. Optimized for consistency, not query speed. Often an aggregate (M.28) or a few normalized tables.
Read Model
/riːd ˈmɒd.əl/
A denormalized, query-shaped view of the data the read side serves from. Multiple read models can coexist, each tuned for one access pattern. Updated asynchronously from write-side changes.
Projection
/prəˈdʒɛk.ʃən/
In CQRS+ES: the process (and resulting read model) that consumes events from the write side and updates a denormalized table. Same concept as in M.28. Rebuilds are possible by replaying.
Eventual Consistency
/ɪˈvɛn.tʃu.əl/
The property that the read side lags the write side by some small window (typically tens to hundreds of ms). Unavoidable in CQRS; must be designed for in UX from the start.
CDC · Change Data Capture
/siː diː siː/
A way to detect changes to a database (via log tailing, triggers, or polling) and emit them as events. Used in CQRS without ES to feed the read side from a traditional write-side database. Debezium is the popular open-source tool.
Read-Your-Writes
/riːd jʊr raɪts/
A consistency property where a user always sees their own changes immediately on subsequent reads. Naturally hard in CQRS due to async sync. Mitigated by reading from the write side for the user's own data, or using "optimistic UI" patterns.
§ 07 — Knowledge check

Five questions.
Choose the model.

Test the CQRS intuition. Click an answer; explanation drops in instantly.

QUESTION 1 OF 5
Loading question...
Score: 0 / 5
5 / 5

Split.

Perfect. Commands, queries, the asymmetry, the trade-offs — all yours. Now: how do you coordinate state changes across multiple aggregates? That's where it gets really interesting.

§ 08 — The recap

Three ideas to
carry forward.

The architectural pattern that takes the read/write asymmetry seriously instead of pretending it isn't there.

i

Reads and writes are different

One model serving both is a compromise neither side wins. CQRS makes the asymmetry explicit and lets each side be tuned for what it actually does.

ii

ES and CQRS are independent

You can have CQRS without event sourcing, ES without CQRS, or both together. The combo is most common, but the patterns answer different questions.

iii

Don't over-apply it

CQRS pays off when reads and writes diverge in shape, scale, or storage. For small CRUD apps, it's just two databases pretending to be one. Pattern-fit beats pattern-prestige.

↓ UP NEXT

M.30 — Sagas &
distributed transactions.

You've split commands from queries. Now: how do you handle a "place order" that spans payment service + inventory service + shipping service — when any of them might fail and there's no global ACID transaction across them? The pattern that does what 2PC never could at scale.

Continue to Module 30 →