Skip to main content

CQRS & Read Models

Level 6: Level 6: Asynchronous & Event-Driven Systemshard30 mincqrsread-modelsprojections

Split commands (validated write model) from queries (denormalized projections built by idempotent event handlers), accept eventual consistency and handle read-your-writes explicitly, rebuild read models by replay, and do not drag in event sourcing unless you separately need it.

One idea: separate the write model from the read models

CQRS (Command Query Responsibility Segregation) is one idea: separate the model you write through from the model(s) you read through. The write side handles commands (intent to change state: "PublishProduct"), runs validation and business invariants, and owns the authoritative data. The read side serves queries from denormalized projections shaped exactly for how the UI reads. They can be different schemas, different databases, even different technologies.

Why separate them

Reads and writes have opposite pressures. A product catalog might take 50 writes/sec (with heavy validation: pricing rules, category constraints, inventory checks) but 500k reads/sec (search, filter, faceted browse). A single normalized schema optimized for write-side integrity forces reads to join 8 tables per page load. CQRS lets the write side stay a clean normalized model in Postgres, while the read side is a denormalized document in Elasticsearch or a materialized Redis view, each scaled independently. You are no longer forced to make one schema good at two conflicting jobs.

How projections stay in sync

The write side, on committing a command, emits an event ("ProductPublished"). Projection handlers consume those events and update the read models. You can have many projections off one event stream: an Elasticsearch index for search, a Redis hash for the product detail page, a Postgres rollup for the admin dashboard, each a different shape for a different query. Handlers must be idempotent (processing the same event twice yields the same result), because at-least-once delivery will redeliver. Store the last processed event offset/version per projection so replays are safe.

CQRS: one write model, many read models
Step 1 / 3

Stage 1 of 3: Commands like PublishProduct carry intent to change state; the write model runs pricing, category, and inventory validation and owns the authoritative data at about 50 writes/sec.

Projections update after the commit, so reads can lag by milliseconds to seconds; handle read-your-writes explicitly with client echo, versioned reads, or a short window of reading from the write model.

The consistency cost: eventual consistency

The projection updates after the write commits, so there is a lag (usually milliseconds, sometimes seconds under load). A user who edits a product and immediately reloads may see stale data. This breaks read-your-writes expectations. Fixes: (1) client echo, where the client optimistically shows its own just-submitted value without waiting for the read model; (2) versioned reads, where the write returns a version and the client polls/reads until the projection has caught up to that version; (3) route the user's own immediately-following read to the write model for a short window. Pick one and state it; hand-waving eventual consistency is a red flag.

Rebuild superpower

Because projections are derived and idempotent, you can drop a read model and replay the event log to rebuild it. That is how you add a new read model months later, fix a projection bug, or migrate the read store: reset the offset to 0 and reprocess. This is the strongest operational reason to adopt CQRS.

Interview nuance: CQRS and event sourcing are often taught together but are independent. You can do CQRS with a plain CRUD write model that emits events (or that a change-data-capture stream like Debezium tails), no event store required. Coupling CQRS to full event sourcing "because they go together" doubles your complexity for no reason if you did not need the event log. Default to CQRS-with-CDC unless audit/temporal needs justify event sourcing too.

Recap: split commands (validated write model) from queries (denormalized projections built by idempotent event handlers), accept eventual consistency and handle read-your-writes explicitly, rebuild read models by replay, and do not drag in event sourcing unless you separately need it.

Apply

Your turn

The task this lesson builds to.

Design read/write separation for a high-read product catalog that has complex write validation: build denormalized read projections updated from write-side events and handle read staleness.

Think about

  1. Why separate the write side (commands, validation) from the read side (denormalized projections)?
  2. How do projections stay in sync, and how do you handle read-your-writes UX?
  3. Why should CQRS not be coupled to event sourcing by default?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the CQRS read-side for Amazon-scale product search: a write model that ingests 100k catalog updates/sec from thousands of seller and inventory services, fanning out to multiple denormalized read models (full-text search, per-region price/availability, a recommendations feature store) while keeping each independently rebuildable and handling seller read-your-writes.

Think about

  1. Why is a single partitioned event log the right fan-out backbone?
  2. How do consistency requirements differ across the read models?
  3. How do you keep a stale in-stock flag from causing oversells?