Skip to content
advanced Phase 9 · Data Architecture: Persistence, CQRS, and Event Sourcing

CQRS Pattern

Split command-side writes from query-side projections and their models.

1h
0 problems
Topic Progress 0%

One Order Table Fighting Two Jobs: Splitting the Model That Updates from the Model That Reads

The orders service serves two very different clients. The checkout API needs to validate an order, apply inventory rules, and write it — a handful of rows, normalized, transactional, latency-critical. The operations dashboard needs 'all open orders for region West with customer name, last event, and SLA countdown, filterable and sortable' — a shape nobody normalized, served to hundreds of internal users. One table tries to do both: the dashboard's joins drag the checkout writes, and every new dashboard column becomes a migration on the transactional path. Command Query Responsibility Segregation is the recognition that these are two models with two responsibilities, so build two.

WRITE side (commands):                 READ side (queries):
  validate invariants                    shaped exactly for one screen
  single aggregate per transaction       denormalized joins, precomputed fields
  normalized schema                      schema owned by the reader's needs
  transactional, linearizable-ish        possibly stale, cacheable, wide
        │                                        ▲
        └── domain events / change stream ───────┘   (the projection feeds the read model)

The insight underneath: a model optimized for correct state transitions and a model optimized for fast reads have contradictory design pressures. Normalization prevents update anomalies (write-side concern); denormalization prevents expensive joins (read-side concern). Constraints and cascades protect invariants (write-side); flattened fields and materialized aggregates serve screens (read-side). Trying to satisfy both in one schema produces the compromise nobody likes — a 20-column table with three indexes that is mediocre at both jobs.

The command side's rules, worth stating in an interview: commands are intent (PlaceOrder, CancelOrder), validated against the aggregate's invariants before anything is persisted; the write model is the system of record, and exactly one service owns writes to it (the one-writer-per-fact rule from Polyglot Persistence returns here). After committing, the service publishes what happened (events via an outbox), and the read side builds its own views.

The read side's freedom is the payoff: the projection can hold a customer_name column joined in from another context, an sla_status computed at ingest, and a JSON blob of recent events — because nothing ever writes through the read model, there is no update anomaly to prevent and no migration to coordinate with the transactional path. It can also be rebuilt from scratch: a projection is a function of the event stream, so a bug fix is 'stop projection, deploy new logic, truncate, replay' — version control for data views.

What CQRS is not, since interviews probe this: it is not a database per se (it can be two tables in one Postgres), it does not require event sourcing (events are the most common feed, but CDC or explicit read-model updates work), and it is not microservices (one service can contain both sides). The pattern is specifically separating the responsibility of the model that mutates state from the model that answers reads — and the moment you say that, the follow-up question is about the costs, which are real and covered later in this topic. One more property is worth internalizing: the split is per bounded context, not per company. Applying CQRS to a simple CRUD service while its neighbors stay single-model is normal; forcing one global pattern across every service is how teams drown in ceremony they cannot cash out.

Projections: Build the Read Model as a Function You Can Delete and Recompute

The read model is a table like any other, but with one property that changes everything about how you operate it: it is derived state. Everything in orders_dashboard can be reconstructed from the system of record — which means migrations become replays, bugs become rebuilds, and 'we added a column wrong' becomes an afternoon instead of an incident. Treat the projection as a function: view = f(events), deterministic and idempotent, and the operational playbook writes itself.

A concrete projection, fed by CDC or domain events:

-- projection: orders_open_dashboard (the ONLY thing the dashboard queries)
-- fed by consumer applying OrderPlaced / OrderShipped / SlaBreached events
INSERT INTO orders_open_dashboard
  (order_id, customer_name, region, sla_status, last_event_at, total)
VALUES (...)
ON CONFLICT (order_id) DO UPDATE SET ...;   -- idempotent upsert, safe on redelivery

The design rules that keep projections honest:

  • Idempotent application. Every event carries an id; the projection records the last-applied position per stream/table and skips or upserts on replay. Redelivery then has no effect — this is the idempotent-consumer discipline applied to your own view (Event-Driven Architecture Patterns).
  • Ordered per key, tolerated-out-of-order globally. Projections process a key's stream in order (partition by entity id) and version-guard transitions (WHERE last_version < :v) so a late event cannot regress a row.
  • Freshness is declared, not hoped. The projection lag (newest event vs newest applied) is a first-class SLO: 'dashboard is at most 2 s behind' is an engineering statement with an alarm on it, not a disclaimer.
  • Rebuild is a supported operation. Keep the bootstrap path exercised: rebuild into a shadow table on a schedule or in CI, compare a sample, swap. A projection you cannot rebuild is not derived state — it is an undocumented second database.
  • Backfill on new consumers. Adding a column means replaying history or re-reading the system of record — plan it with the retention window in mind (log retention caps how far back you can rebuild; beyond that, snapshot + replay the tail).

The most valuable property in incidents: the read model can be wrong without the business being wrong. A dashboard showing a slightly stale total is a product annoyance; the ledger being wrong is an incident. Because the two are physically separate, you can degrade, rebuild, and pause one while the other keeps its guarantees — which is also why read projections are allowed to be eventually consistent, cached, and aggressively denormalized.

This structure also answers the classic scale question with real mechanics: reads scale by adding projection instances and read replicas with no impact on the write path; writes scale vertically or by partitioning the write model by aggregate. The two sides can fail independently — a dashboard rebuild storm cannot corrupt orders, and a checkout burst cannot make the dashboard worse than its declared lag. That independence is the same shape as Transactional vs Analytical Workloads, generalized from 'analytics vs serving' to 'every read view vs the write model'. The final chapter prices the pattern honestly, including when not to use it.

When Not to CQRS: The Costs, the Failure Modes, and the Simpler Alternatives

CQRS has a reputation for being over-engineered, and the reputation is deserved when teams adopt it for a CRUD app. The honest cost list, so you can price it before adopting:

Cost What it means in practice
Eventual read models a user may create an order and see it missing on refresh; needs UX handling (optimistic UI, 'pending' states)
Two schemas, two migrations the write schema and each projection schema evolve separately; versioning discipline required
Projection lag as an SLO monitoring, alerting, and a lag-reduction runbook become owned work
Rebuild time new projection logic means replaying history; at scale that is an hours-long job you must rehearse
Debugging across the split 'why is the screen wrong?' spans producer, relay, and projection — correlation IDs are mandatory
Operational surface another consumer to run, another store to back up, another lag to page on

The failure modes to name in design review: projections that silently stop (consumer healthy, lag growing — alarm on lag age, not just consumer liveness); non-idempotent projections that double-count on redelivery (the classic revenue bug); hidden second sources of truth where someone writes to the projection directly and it drifts; and event schemas so coupled to the write model that refactors break every consumer (the contract problem Schema Evolution and Compatibility exists to solve).

The decision rule, stated as an interview answer: CQRS pays when read and write shapes diverge significantly or scale independently; it is a tax when they do not. Concretely, adopt it when at least one is true — reads are far heavier than writes or need a different shape (dashboards, search-backed views); the domain has complex aggregates whose write model does not map to screens (logistics, insurance); or multiple read views over one write model are proliferating (three screens, four APIs). Skip it for straightforward CRUD where one normalized table plus indexes meets every requirement; the incremental middle ground — a materialized view, a read replica with rollups, a cached endpoint — delivers most of the benefit with none of the split.

A useful framing for the design doc: CQRS is the pattern that makes the read side a first-class, rebuildable artifact. Once you accept that framing, the surrounding decisions follow: the write model stays small and invariant-focused; projections are functions of a durable log; freshness is an SLO; and nothing writes to a view. Teams that adopt the split while keeping the discipline (idempotent projections, declared lag, rebuild drills) get the benefits teams complain about not having — and the ones that adopt the brand without the discipline get a CRUD app with extra steps. A useful rehearsal for the design review: ask 'what happens the day after a projection bug ships?' If the answer is 'we hand-edit rows', the team has adopted the split without the rebuild discipline, and the read model has quietly become a second source of truth. Next, the pattern's natural companion: what happens when the write model itself is an append-only log of facts — Event Sourcing.

Quiz

1. What is the central claim of CQRS?

Question 1 options

2. Why can a CQRS read projection be denormalized freely?

Question 2 options

3. A projection consumer crashes after applying an event but before committing its offset. What must the projection do on redelivery?

Question 3 options

4. When is CQRS the wrong choice?

Question 4 options

Flashcards

Question

Define CQRS in one sentence and list three things it is not.

Answer

Separate the model that validates and updates state (commands, invariants, normalized, single writer) from the model that answers reads (shaped per view, denormalized, rebuildable), fed by events/CDC. It is not: a requirement for separate databases, a requirement for event sourcing, or microservices — two tables in one Postgres can be CQRS.

Question

Why can read projections denormalize, and what replaces the guarantees they would otherwise rely on?

Answer

They accept no writes, so update anomalies and write amplification do not apply. What replaces normalization's guarantees is the projection's contract: idempotent upserts keyed by entity/event id, per-key ordering with version guards, a declared freshness SLO with lag alarms, and a rehearsed rebuild path from the durable log — derived state is corrected by recomputation, not by in-place repair.

Question

How do read and write models scale and fail independently under asymmetric load?

Answer

Reads scale with projection instances, read replicas, and caches without touching the write path; writes scale vertically or by partitioning aggregates. Failures isolate too: a rebuild storm degrades only the view (within its lag SLO), while a checkout burst cannot make reads worse than declared lag — the write model's guarantees stay intact either way.

Question

Name four CQRS failure modes and the discipline that prevents each.

Answer

Projections silently stopping → alarm on lag age, not consumer liveness. Non-idempotent projections double-counting on redelivery → upsert by key + recorded position/version. Someone writing directly to the read model (drift) → enforce one-writer-per-fact, treat views as disposable. Schemas coupled to the write model breaking consumers → versioned event contracts with compatibility enforcement.

Question

When does CQRS pay, and what is the incremental middle ground?

Answer

It pays when read and write shapes diverge strongly (dashboards, search-backed views) or scale profiles differ, or when several read views over one write model proliferate. The middle ground for everything else: materialized views, read replicas with rollups, or cached endpoints — most of the benefit without the dual schemas, lag SLOs, and rebuild drills.

Revision Notes

Key Takeaways

  • 1. CQRS separates two contradictory optimization targets: invariant-enforcing writes and screen-shaped reads, each with its own schema and lifecycle
  • 2. A projection is a function of a durable log — disposable, rebuildable, and correctable by replay rather than repair
  • 3. Read-side freedom is paid for with contracts: idempotent application, version guards, a declared lag SLO, and rehearsed rebuilds
  • 4. Reads and writes scale and fail independently, so view degradation never becomes ledger corruption
  • 5. Without shape divergence CQRS is a CRUD app with extra steps — start from materialized views and escalate only when the shapes truly split

Interview Tips

  • • "Explain CQRS" — command model enforces invariants, query model is shaped for reads, events/CDC connect them; then immediately list what it does not require (separate DB, event sourcing, services)
  • • "The dashboard showed a wrong total" — walk the projection discipline: idempotent upsert, per-key ordering, version guards, lag alarms, and the rebuild drill that fixes the class of bug
  • • "How do you add a field to the read model?" — deploy projection logic, backfill via log replay within retention (snapshot + tail beyond it), swap the shadow table — no migration on the write path
  • • "Would you CQRS this service?" — test for shape divergence and asymmetric scale; if absent, propose materialized views/caching first and name the costs you are avoiding

Cheat Sheet

CQRS

Split:

WRITE (commands): validate invariants · normalized · one owner · system of record
                  commit then publish events (outbox)
READ  (queries):  shaped for one screen · denormalized · no write path · rebuildable
                  fed by events/CDC — view = f(log), deterministic + idempotent

Not: separate databases (required) · event sourcing (required) · microservices.

Projection rules:

  • idempotent upserts keyed on entity id + recorded applied position (redelivery = no-op)
  • order per key (partition by entity); version guard WHERE last_version < :v
  • freshness = declared SLO with lag alarm (newest event vs newest applied)
  • rebuild rehearsed: shadow-table rebuild + sample compare + swap
  • new consumer ⇒ backfill bounded by log retention; beyond it, snapshot + replay tail

Why denormalization is safe: no writes ⇒ no update anomalies; wrong view ≠ wrong business
(mis-stale dashboard is UX; wrong ledger is an incident) ⇒ degrade/rebuild views independently.

Costs: eventual reads (UX must handle) · dual schemas/migrations · lag SLO · rehearsed
rebuilds · cross-split debugging (correlation IDs mandatory) · another consumer to operate.

Failure modes → discipline: silent projection stop → lag-age alarm · double-count on
redeliver → idempotent upsert · direct writes to views → one-writer-per-fact · schema coupling →
versioned contracts.

Decision: adopt when read/write shapes diverge or scale asymmetrically; else materialized
views / cached endpoints. Interview line: the pattern makes the READ side a first-class,
rebuildable artifact.

Continue Learning