Architecture Patterns
CQRS
One schema trying to be good at writes and good at reads at the same time, and why splitting them into two models means the read side is never perfectly caught up.
Architecture PatternsAdvanced5 min read
After this lesson, you should be able to:
- Explain what CQRS actually splits, a read model versus a write model, and why one shared schema struggles to serve both well.
- Distinguish what CQRS costs, propagation lag, more moving parts, from what it buys, a read model shaped for the queries that actually run.
- Recognize that CQRS doesn't require a second database technology, a materialized view or second table can qualify.
- Compare CQRS's read-lag problem to a database replica's replication lag, the same underlying cause, one layer up the stack.
At a Glance
- A model optimized for correct, fast writes and a model optimized for fast, flexible reads pull in opposite directions, and one schema serving both ends up compromised at both.
- CQRS splits the write model, the source of truth, from the read model, a shaped, denormalized copy built for the queries that actually get run.
- The read model is a copy, not the source. It updates after the write, not during it, so there's always a window where a read can be one step behind reality.
01 The Problem
A hotel booking system's database has bookings, rooms, and guests tables, normalized, so a booking can't reference a room that doesn't exist and two bookings can't double-book the same room on the same night, enforced by constraints and a transaction at write time. The write path stays fast and correct: check availability, insert the booking, done.
The operations dashboard needs occupancy rate by room type, by week, across every property, three joins and a GROUP BY over months of history, refreshed by dozens of managers throughout the day. That query is slow on the normalized schema, and it competes for the same indexes and locks the booking write path needs to stay fast.
Two bad fixes. Add indexes to make the dashboard query fast, and now every booking write updates those indexes too, slowing down the one path that has to stay fast under concurrent booking attempts. Denormalize the schema instead, pre-aggregating occupancy onto the bookings table, and now every booking write has to keep that aggregate correct too, inside the same transaction that's already enforcing no-double-booking. A fast, focused write turns slow and fragile, carrying reporting logic it never needed.
This is the actual tension: a schema shaped for correct, fast writes and a schema shaped for fast, flexible reads pull in opposite directions. One table can't be optimized for both at once.
02 What It Is
CQRS, command query responsibility segregation, splits the write model from the read model. The write model, the command side, is the source of truth: normalized, transactional, optimized for correctness under concurrent writes. The read model, the query side, is a separate, denormalized, shaped copy of the data, optimized for whatever queries actually run against it, indexed however makes those queries fast, with zero concern for write correctness because it never accepts a write directly.
The two models existing separately is the easy part. The read model isn't the source of truth, it's a copy, and copies update after the original changes, not during it. That means the read model can lag behind the write model, sometimes by milliseconds, sometimes by more, and anything reading from it has to accept it might not reflect the write that just happened a moment ago. CQRS doesn't remove that lag. It just makes it visible and intentional instead of hiding it inside a single overloaded table trying to be both.
In Plain Terms
A restaurant kitchen keeps its own working notes on index cards, scrawled fast, correct in the moment, exactly what each table ordered. The dining room posts a clean printed summary on a board for the host to check table status at a glance. The board isn't updated card by card in real time, it's updated after each ticket clears, so for a few seconds the board can say a table is still cooking when the kitchen already knows it's plated. Nobody's lying. The board is just a copy, and copies update after the fact.
03 How It Works
Rendering diagram…
04 Trade-offs
| One shared model | CQRS (separate read/write models) | |
|---|---|---|
| Write path | Competes with read-optimized indexes and denormalized fields on every write | Only carries what correctness needs; nothing added for a query it'll never answer |
| Read path | Constrained to whatever shape serves the write path well | Shaped and indexed however the actual queries need, independent of the write schema |
| Consistency | A read always reflects the latest write; they're the same table | A read can lag behind the latest write by however long propagation takes |
| Complexity | One model, one thing to reason about | Two models plus the mechanism that keeps them in sync, more moving parts |
Watch out
It's tempting to reach for CQRS because a dashboard query is slow, as if splitting the models were a performance trick with no downside. The performance win is real, but the actual thing you're signing up for is eventual consistency on every read that goes through the read model: a user who just made a booking and immediately checks the dashboard can see a stale count, not because anything broke, but because the read model hasn't caught up yet. If your reads need to reflect a write that happened a second ago, you've picked the wrong side of this trade, not found a bug in it.
05 Where You'll Actually See This
The URL shortener case study's Redis-in-front-of-Postgres design is a narrow version of this: Postgres is the source of truth, Redis is a read-optimized copy shaped for exactly one query, look up by code. It isn't full CQRS, Redis isn't reshaped or aggregated differently, just cached, but the underlying shape, a separate, optimized read path that can lag the source of truth, is the same idea.
Event-driven architecture is often the actual mechanism that updates the read model: the write model publishes "booking created," and whatever builds the read model subscribes and updates its own copy, the same publish-and-forget shape covered there, just with "update a denormalized read model" as the subscriber's job. And database replication's staleness problem is this same shape one layer down: an asynchronous replica lags the primary for the same reason a CQRS read model lags the write model, the update has to travel somewhere before it's visible. CQRS just does it at the application's data-model level instead of the database's storage engine, and it can reshape the data on the way, not just copy it.
How to reason about it
When asked whether a system needs CQRS, don't just say "it separates reads and writes." Say what's actually driving it, a read shape the write model genuinely can't serve well, and say how stale the read side is allowed to be before it's a real problem, not a theoretical one. An answer that can't name a specific query the write model struggles with is reaching for a pattern, not solving a problem that actually exists.
06 Quick Check
A team hears about CQRS and assumes it requires standing up a separate database technology, like adding Elasticsearch or MongoDB alongside their existing Postgres. Is that requirement accurate?