Skip to main content

CQRS

Editorial

Introduction

Database per Service ended with a thread left deliberately hanging. When API composition gets too slow or too awkward, it said, build your own copy of the data, shaped for your reads, fed by the owner's events. This lesson picks that thread up and follows it to a pattern with a forbidding acronym and a simple heart.

Start with a tension you have been circling for two courses. The model that is good at being written to and the model that is good at being read from are not the same model. Writing wants rules: normalized tables, foreign keys, invariants checked in one place, small careful transactions. Reading wants answers: the product page, pre-joined; the search results, pre-indexed; the top-ten list, pre-computed. For two courses you have patched the gap from both ends, denormalizing here, caching there, adding a replica when the reports got heavy.

CQRS, Command Query Responsibility Segregation, is the pattern that stops patching and names the split: keep one model whose only job is to change things correctly, and build as many read models as you have questions, each shaped exactly like its answer. It is also, by its own inventors' warnings, one of the most misused patterns in this course. So this lesson carries two jobs: teach the split properly, and teach you where it belongs, which is fewer places than the internet suggests.

Infographic titled: one model, two masters. The left panel, the stretch, shows one amber model box pulled between nine sky read arrows that want screen shapes and one green write arrow that wants the rules, with the warning that every index for reads taxes every write. The right panel, the split, shows a command model labelled the vault feeding an amber events rail down to three sky read models, page view pre-joined, search index full-text, and top-ten list pre-computed. The dark band states the pattern: one model changes things, one read model per question, and every expensive thought is thought at write time. A closing line names the price: the windows lag the vault, and authors notice.

One Model, Two Masters

Picture the reviews feature of a shop. One reviews table, normalized the way the transactions course taught: review rows, user rows, product rows, foreign keys between them. It is a good write model. Every invariant, one review per user per product, no orphaned reviews, lives in one place and is enforced by the schema itself.

Now watch what reads want from it. The product page wants twenty reviews with usernames and avatars: a three-way join. The search box wants full-text matching, which a relational index does badly, as Full-Text Search Databases explained. The "most helpful" ranking wants an aggregate over votes. And reads outnumber writes brutally, ten to one is typical, so the model spends ninety percent of its life doing the thing it was not shaped for.

The standard first aid makes it worse in slow motion. You add indexes for the reads, and Indexes Deep-Dive already showed you the tax: every index speeds one query and slows every write. You denormalize a little, and now the invariants live in two places. You bolt a cache in front, and inherit Cache Invalidation: The Second-Hardest Problem. Each patch borrows against the other master's account.

The tension is not incompetence. It is structural: one shape cannot be optimal for guarding changes and for answering questions at the same time, any more than one building can be optimal as a vault and as a shop window. The patches were attempts to make the vault easier to look into. At some point, the honest move is to build the shop window as its own thing.

The Split: CQS Grown Up

The idea has clean roots. Bertrand Meyer's old design principle, command-query separation, said a method should either change state or return data, never both, because code you can reason about keeps its questions innocent. Around 2010, Greg Young lifted that principle one level: not two kinds of method on one object, but two kinds of model. His definition is almost dry: the same commands and queries Meyer described, but split into separate objects, one holding the commands, one holding the queries. Everything else in this lesson falls out of taking that sentence architecturally seriously.

The command model owns change. It receives commands, post_review, mark_helpful, validates them against the rules, enforces every invariant, and commits. It stays normalized, guarded, and small, because that is what correctness under change needs. It never renders a page, so it never grows a join it does not need. It is the vault, and Database per Service already gave it a door.

The read models, plural, own answers. The product-page model is a denormalized table where each row already carries username, avatar, and vote count: the page is one indexed lookup. The search model is a full-text index. The ranking model is a pre-computed aggregate. Each is disposable, rebuildable, and shaped like exactly one family of questions, the discipline Denormalization taught, now with a clean source of truth to be derived from.

Reads become boring, and that is the entire point: no joins at request time, no aggregation under load, no clever query planner heroics. Every expensive thought was already thought when the data was written.

The Dial You Already Turn

Here is the reframe that makes CQRS stop being exotic: separation of the read path from the write path is a dial, not a leap, and you have been turning it for two courses.

Notch one: one model. The default. Right answer for most systems, most of the time. Nothing to build, nothing to lag, nothing to drift.

Notch two: read replicas. Read Replicas & Replication Lag moved reads onto separate hardware, same schema. Different machines, one shape. You accepted a staleness window in exchange for read capacity, and learned the lag arithmetic that governs everything at the higher notches too.

Notch three: materialized views. Materialized Views changed the shape: a pre-joined, pre-aggregated table the database maintains from the base tables. Look at it with today's eyes: a differently shaped read model, derived from the write model, refreshed with a delay. That is CQRS conducted entirely inside one database engine. You were doing the pattern before you knew its name.

Notch four: the full pattern. Separate read stores, possibly different engines, a search index here, a key-value view there, fed by events, maintained by your own code. Maximum shape freedom, maximum machinery.

Framed as a dial, the design question stops being "should we do CQRS?", which invites ideology, and becomes "which notch does this context earn?", which invites arithmetic: read-to-write ratio, how different the question shapes are from the storage shape, and how much staleness the answers can tolerate. Most contexts live their whole lives at notch one or two, and should.

One near neighbour deserves a sentence, because every beginner asks it: is a read model just a cache? Close kin, different job. A cache answers the same question faster, and its curse is invalidation. A read model answers a different question, pre-joined and pre-computed into a new shape, and when it goes wrong you do not invalidate it, you rebuild it from the source. The cache is a shortcut; the read model is a second, purpose-built answer.

Four numbered notches on a rising dial titled: the dial you already turn. Notch one, one model, the default, stamped most systems. Notch two, read replicas, same shape on more hardware, stamped with the read replicas lesson. Notch three, materialized views, with the reveal stamp CQRS inside, credited to the materialized views lesson. Notch four, full CQRS with events and shaped stores, stamped this lesson. Each notch draws a write box and an increasingly separate, increasingly tilted read box. The dark band reframes the question: not should we do CQRS, but which notch does this context earn, and most contexts live at notch one or two, and should.

The Pipeline, and Its Lag

At notch four, something has to carry changes from the vault to the shop windows, and you already own every part of it.

A command arrives and the command model validates and commits it: local, ACID, boring, exactly as the write side deserves. The committed change then leaves the building as an event, and the safe way out is the machinery from Outbox + CDC: Reliable Events from Your Database, because the dual-write trap does not care how fashionable your architecture is. The event rides a stream, the log shape from Queue Models: Broker (RabbitMQ) vs Log (Kafka), to the projectors: small consumers, one per read model, whose whole job is "given this event, update my view". The page projector inserts a pre-joined row. The search projector updates the index. The ranking projector bumps an aggregate.

Now the honest part. That hop from commit to projection takes time, milliseconds on a good day, seconds under load, and while it runs, the read models are behind the truth. This is not a new demon: it is replication lag wearing a new coat, the same physics you measured in Read Replicas & Replication Lag. What is new is that the window is now per read model: the page view might lag by fifty milliseconds, the search index by two seconds, the analytics rollup by a minute, and each of those is a choice you place on The Consistency Spectrum deliberately, per question, instead of one setting for the whole system.

One more honest consequence: there are now several stores where there was one. Each read model is one more thing that runs, fills disks, and gets paged for. The ops-tax line from the mesh and the polyglot lesson applies unchanged, and it is one of the real reasons the dial should not be turned casually.

The pipeline titled: commit first, project after. A post-review command enters the command model, which validates and commits, then the event leaves through the outbox. From there dashed projector lines fan to three read models with their own staleness stamps: the page view about fifty milliseconds behind, the search index about two seconds behind, and analytics about a minute behind. A red dashed box under the outbox marks where the lag lives. A sky band states that staleness becomes a choice made per read model, and the dark band closes: replication lag in a new coat, worn on purpose.

The Drill: Break Your Own Read

Two experiences, in order.

First, feel the war. One model serves the reviews feature while reads outnumber writes nine to one. Add the index the reads are begging for, and watch what it does to the writes. There is no setting that makes both meters happy. That is the point.

Then split it, and do something no diagram can do for you: type a real review, in your own words, and post it. Watch your command validate, commit, and set off down the pipeline while the projector lag bar fills. Now read the product page immediately, before the bar completes, and meet the pattern's sharpest edge personally: the page is fast, the page is fine, and your own review is not on it. Then turn on a fix and watch your words arrive. Keep the lag dial in hand while you do it; the edge gets sharper the slower the projector runs.

Feel one model serve two masters, then split it: type a real review, watch it travel the pipeline, and catch the moment your own words go missing from the page.

Read Your Own Writes

Back in Caches & Databases: Read-Your-Writes Coherence, a promise was planted: the full version of this problem would return in the patterns course. This is it, and the drill just made it personal.

Eventual consistency between strangers is almost always fine. Another shopper seeing your review two seconds late is not a problem anyone notices. But the author reading the page and not finding their own words does not experience "eventual consistency". They experience data loss, and they retry, or file a bug, or leave. The rule worth memorising: staleness is a tolerance others have and authors do not.

Three fixes, in rising order of machinery, all cousins of the coherence techniques you already know.

Pin the author to the truth. For a short window after a write, serve that user's reads from the command side (or its immediate replica). Everyone else gets the fast read models; the author briefly gets the vault. Simple, effective, and the read model can catch up in peace.

Wait for your version. The commit hands back a version token; the client presents it on the next read, and the read side serves only when its projection has caught up to that version, waiting a few milliseconds if needed. Precise, at the cost of carrying tokens around.

Let the client fake it. The UI appends the review it just posted to the page locally, without waiting for any backend at all. By the next real page load, the projector has long caught up. Most large sites you use every day are doing exactly this to you, and it is honest work.

Whichever fix you choose, choose it at design time. The missing-review bug discovered in production gets patched with whichever hack is nearest; the same behaviour designed up front is two days of work and a better product.

Diagram titled: where did my review go? An author writes and the vault has it, committed, while a dashed amber arrow shows the projector still carrying it toward the page. The author reads two hundred milliseconds later and the page, served from the read model, shows mira and tomas with checkmarks but a dashed red empty slot reading yours: not here, captioned: to the author, data loss. Below, two fix cards: pin the author, routing their reads briefly to the vault itself after a write, and wait for version, where the read holds until the projection reaches the author's write. The dark band states the rule: staleness is a tolerance others have and authors do not.

The Bill, and Fowler's Warning

The pattern's costs are structural, so state them the way an architect would in review.

Two models must now agree. The read models are derived data, and derivations drift: a projector bug, a missed event, a partial rebuild, and the page quietly shows something the vault never said. Nothing crashes; correctness just erodes. Mature CQRS systems run reconciliation: periodically re-derive samples from the write model and diff them against the read models, the same trust-but-verify instinct you met in shadow comparisons.

Read models die and must be rebuilt. A new question needs a new read model; a projector bug corrupts an old one. The recovery move is always the same: point a fresh projector at the history and replay it into the new shape. Which quietly assumes something profound: that you still have the history, every change, in order, from the beginning. Hold that thought for one more section.

And the complexity is real, which is where the pattern's own literature turns unusually blunt. Martin Fowler's assessment, from watching real projects: CQRS is a significant mental leap, most uses of it he has seen were mistakes, and it belongs only on specific bounded contexts, never smeared across a system. The suitable case, he says plainly, is the minority case. Take the warning at face value: the reviews context, ten-to-one reads, three differently shaped questions, staleness-tolerant, earns notch four. The billing context next door, balanced reads and writes, one shape, zero staleness tolerance, stays at notch one forever, and putting CQRS there is how a team spends six months building a slower, less correct version of a Postgres table.

The dial, one context at a time. Never the whole system at once.

And one fusion to undo before the handoff, because the internet performs it constantly: CQRS and event sourcing travel together so often that people weld them into one pattern. They are independent. Everything in this lesson works with a plain relational write model, and most CQRS systems in production run exactly that. What comes next is a further, separate step.

A two-by-two map of bounded contexts titled: one context at a time, if at all. Reviews, highlighted green, runs full CQRS with a small vault feeding a page view and a search model, justified by ten-to-one reads and tolerance for lag. Billing, auth and inventory each sit muted on a single write-equals-read model with their reasons: no staleness tolerance, one shape, and reads roughly equal to writes. The dark band delivers Fowler's field verdict: most uses he watched were mistakes, and the suitable case is the minority case.

Takeaways

  • One shape cannot serve two masters. Writing wants normalized and guarded; reading wants pre-joined, pre-indexed, pre-computed. The war is structural, and every patch borrows from the other side.
  • CQRS is CQS grown up. One command model that changes things correctly; as many read models as there are question families, each shaped like its answer.
  • It is a dial, not a leap. Replicas were notch two. Materialized views were CQRS inside one engine. The full pattern just adds shape freedom and machinery. Ask which notch a context earns, not whether to "do CQRS".
  • The pipeline is parts you own. Outbox out of the vault, events on a log, projectors into the views. The commit-to-projection hop is replication lag in a new coat, now chosen per read model.
  • Authors do not tolerate staleness. Pin them to the truth, wait for their version, or let the client fake it, and decide at design time.
  • Derived data drifts; reconcile it. And rebuilding a read model means replaying history, which assumes you kept it.
  • Believe Fowler's warning. The suitable case is the minority case. One bounded context at a time, earned by read ratio, shape distance, and staleness tolerance.

One thread is now impossible to ignore. Rebuilding read models worked because the events were all still there, in order, from the beginning. The write model kept its current state; the log kept the whole story. Which raises a question that sounds almost mischievous: if the log can rebuild any state you want, whenever you want... why is the state the source of truth and not the log? That inversion is Event Sourcing.