</> MAANG.io
system design Β· 201

Intermediate

Master system design interviews with scalable architecture patterns, distributed systems, and real-world design challenges.

0/124 solved 0% complete

CQRS (Command Query Responsibility Segregation) β€” Part 1: Foundations & Core Concepts

maang.io System Design Series


What is this?

Picture a busy restaurant on a Friday night. There are really two machines running at once, and they are shaped completely differently.

The first is the kitchen. When you order the seared salmon, a ticket flies back and the kitchen does work: it checks whether there's salmon left, fires the pan, plates it, marks the ticket done. The kitchen is all about correctness and rules β€” no salmon means no salmon, an order once fired can't un-fire. It's careful, transactional, and it changes the state of the world.

The second machine is the order-status board on the wall β€” the big screen that says "Table 12: preparing… Table 12: ready." Customers stare at it constantly. That board does no cooking and enforces no rules. Its only job is to be glanceable: pre-formatted, instantly readable, optimized for a thousand hungry eyes. It's a projection of what the kitchen has done, and it's allowed to lag the kitchen by a second or two β€” nobody minds if "ready" appears on screen a moment after the plate actually hits the pass.

Here's the insight: the kitchen and the board want opposite things. The kitchen wants strict rules and one authoritative version of the truth. The board wants speed, denormalized layout, and the freedom to be copied onto ten screens. If you force both jobs through one data model, you compromise both. CQRS is the decision to stop compromising β€” to split the "change the world" path (the kitchen) from the "show me the world" path (the board) into two separate models, each tuned for its own job.

The one-line idea: Split your system into a write side that handles commands (change state, enforce rules β€” the kitchen) and one or more read sides that answer queries (return pre-shaped data fast β€” the order board), so each can be modeled, stored, and scaled independently.

The name unpacks cleanly. CQRS = Command Query Responsibility Segregation. Commands change state ("place order," "cancel order"). Queries read state ("show my orders"). Segregation means you give them separate responsibilities, separate models, and often separate datastores β€” instead of the usual single model that tries to serve both.


1. The problem CQRS solves β€” what hurt before

Most systems start the normal way: one model, one database, shared by reads and writes. You have an Order table. You INSERT into it when someone buys, and you SELECT from it when someone views their history. This is fine β€” until it isn't. Three specific pains push teams toward CQRS.

Pain 1 β€” reads and writes have wildly different scale. An e-commerce site might take 5,000 orders/sec but serve 80,000 order-status page views/sec. That's a 16:1 read/write ratio. With one shared model you must scale the whole thing for the reads, even though the writes are the part that's hard to scale (they need locking, transactions, invariants). You're paying to scale the expensive side just to satisfy the cheap side.

Pain 2 β€” the perfect write shape is the worst read shape. To keep writes correct, you normalize: orders here, line-items there, customers over there, addresses somewhere else. Now the "show my order" page needs a five-table JOIN, computed on every request. The shape that makes writes safe (normalized, no duplication) makes reads slow (joins, aggregation). You can't optimize both in one schema β€” every index you add to speed a read slows a write, and vice versa.

Pain 3 β€” one model can't be smart and fast at once. A rich domain (insurance underwriting, trading, logistics) needs a write model full of business rules, validations, and state machines. But the screens that display it just want flat, ready-to-render rows. Cramming decades of business logic and a fast display format into the same objects makes both muddy.

πŸ’‘ The tell that you're feeling these pains: your read queries are drowning in joins and caching hacks, your write transactions are fighting lock contention, and every change to one side risks breaking the other. CQRS is the structural fix β€” stop sharing the model.

Before CQRS, the usual bandages were read replicas (covered in the Replication topic) and caches (the caching chapter in the 101 tier). Those help, but they still serve the same normalized shape β€” a read replica of a five-join schema is still five joins, just on another box. CQRS goes further: it lets the read side store data in a completely different, pre-joined shape, purpose-built for the query.


2. Core concepts & vocabulary β€” the mental model

Let's name the pieces, using the restaurant throughout.

Command β€” an instruction to change state, named in the imperative: PlaceOrder, CancelOrder, ApplyDiscount. A command is a request that can be rejected ("sorry, out of salmon"). It never returns data to display β€” at most it returns success/failure and maybe an ID. This is a customer placing an order.

Query β€” a request to read state without changing anything: GetOrderStatus, ListMyOrders. A query never has side effects and always returns data. This is a customer reading the status board.

πŸ’‘ This split β€” "asking a question should never change the answer" β€” is an old programming principle called Command-Query Separation (CQS), coined by Bertrand Meyer at the method level. CQRS takes that same idea and scales it up to the architecture level: separate models, not just separate methods.

Write model (command side) β€” the model that processes commands. It holds the business rules and is the source of truth. In our restaurant, this is the kitchen: it knows the real state (what's actually cooking, what's actually out of stock). It's usually normalized and transactional.

Read model / projection (query side) β€” a denormalized, pre-computed view shaped exactly for a specific query. This is the order-status board. Crucially, a read model is derived β€” it's built from what the write side produced, and it can be thrown away and rebuilt at any time because it holds no truth of its own. You can have many read models from one write model: a customer board, a kitchen-expo board, a manager's daily-totals board β€” same underlying events, three different projections.

The synchronization channel β€” the mechanism that carries changes from the write side to the read side, so the board reflects the kitchen. Usually this is asynchronous β€” a message bus or event stream (see the Asynchronous Messaging and Publish–Subscribe Pattern topics). The write side emits a change; a projector (a small consumer) picks it up and updates the read model.

Here is the whole shape in one picture:

2. Core concepts & vocabulary β€” the mental model

Read the arrows: commands go left-to-right into the write model; queries hit the read models directly. The two sides never share a table. They meet only through the async channel β€” and that meeting is where the famous eventual-consistency gap lives (the board briefly trails the kitchen), which we'll handle head-on in Part 2 and Part 3.


3. How you actually use it β€” a concrete example

The simplest way to feel CQRS is at the API and schema level. Watch how the write endpoint and the read endpoint stop looking alike.

The command side β€” an endpoint that does something. It takes an intent, validates it, and returns almost nothing:

POST /orders
{ "customerId": "c-99", "items": [{ "sku": "salmon", "qty": 1 }] }

β†’ 202 Accepted
  { "orderId": "o-123" }        # an ID and "we've got it" β€” no display data

Behind it, the write model is normalized for correctness:

-- Write side: normalized, transactional, enforces rules
orders(order_id PK, customer_id, status, created_at)
order_items(order_id FK, sku, qty, unit_price)
inventory(sku PK, qty_available)   -- the PlaceOrder command decrements this

The query side β€” an endpoint that returns something, backed by a table that is already the exact shape the screen needs. No joins at read time:

GET /orders/o-123

β†’ 200 OK
  { "orderId": "o-123", "status": "preparing",
    "customerName": "Ada Lovelace",       # pre-joined from customers
    "lines": [{ "name": "Seared Salmon", "qty": 1, "price": 24.00 }],
    "total": 24.00 }                        # pre-computed, not summed at read time
-- Read side: ONE denormalized row per order, ready to render
order_view(order_id PK, status, customer_name, lines_json, total)

Notice what changed. The read table has customer_name copied in (denormalized), the line items flattened into JSON, and the total pre-summed. Reading it is a single primary-key lookup β€” no joins, no aggregation, trivially cacheable and trivially replicable. The projector filled in customer_name and total when the order was placed, moving that work off the hot read path and onto the (much rarer) write path. That's the whole trade in miniature: do the joining and computing once, on write, so every read is free.

⚠️ The 202 Accepted (not 200 OK) is a deliberate signal. It says "I've accepted your command, but the read side may not show it yet." If the client immediately does GET /orders/o-123, the projector might not have run β€” a 404 or stale status for a few milliseconds. Designing the UX for that gap is the single most important practical skill in CQRS, and Part 3 is where we master it.


4. When to reach for CQRS β€” and when it's overkill

CQRS is a power tool. On the right problem it's transformative; on the wrong one it's pure ceremony. Reach for it when you see these signals:

  • Big, asymmetric read/write ratio where the sides need to scale independently (that 16:1 order system).
  • A rich, rule-heavy write domain whose display needs are simple and flat β€” the write model wants to be complex, the read model wants to be dumb.
  • Many different views of the same data β€” a customer view, an ops dashboard, a search index, an analytics rollup β€” each best served by its own purpose-built store (SQL for one, Elasticsearch for search, Redis for a hot cache).
  • You want an audit log / time-travel β€” which pairs CQRS with Event Sourcing, where the write side stores an append-only log of every change (Part 2).

And β€” just as important for a senior engineer β€” do not reach for it when:

  • 🚫 It's basic CRUD. If reads and writes share a shape and scale together, one model and a read replica is simpler, cheaper, and correct. CQRS here is over-engineering β€” you've added an async channel, eventual consistency, and two models to maintain, and bought nothing.
  • 🚫 Your team can't stomach eventual consistency. If the product genuinely needs read-your-own-write everywhere, instantly, the async gap will fight you constantly.
  • 🚫 The domain is small. The operational cost (a message bus, projectors, monitoring lag) only pays off at real scale or real domain complexity.

πŸ’‘ Rule of thumb: CQRS is not all-or-nothing per system β€” it's per bounded context. A mature architecture uses full CQRS + Event Sourcing on the two or three complex, high-value contexts (orders, payments, inventory) and plain CRUD everywhere else (user settings, the FAQ page). Applying CQRS uniformly across a whole app is the classic rookie over-reach.

If you need… Reach for…
Same shape, reads and writes scale together One model + a read replica (Replication topic)
Faster reads of the same shape A cache in front (caching chapter, 101)
Independent scaling + different read shapes CQRS
CQRS + a full audit trail / rebuildable views CQRS + Event Sourcing
Cross-service atomic writes Distributed Transactions / Saga β€” not CQRS

5. Key takeaways

  1. CQRS splits one model into two responsibilities: a write side that processes commands (changes state, enforces rules, the source of truth β€” the kitchen) and one or more read sides that answer queries (denormalized, pre-computed, fast β€” the order board). They don't share a table; they meet through an async channel.
  2. It solves three specific pains: reads and writes that need to scale independently, a normalized write shape that makes reads slow (joins), and a rich write domain that shouldn't be tangled with dumb-and-fast display models.
  3. Read models are derived and disposable β€” projections built from what the write side produced. You can have many of them (customer view, search index, analytics rollup), each in its own best-fit store, and rebuild any of them from scratch.
  4. The cost is the eventual-consistency gap: the read side briefly trails the write side (the board lags the kitchen), which is why command endpoints often return 202 Accepted. Handling that gap in the UX is the core practical skill.
  5. Don't use it for CRUD or small domains β€” it's a per-bounded-context choice, best applied surgically to the complex, high-scale, multi-view parts of a system, not spread uniformly.

Next, Part 2 β€” Deep Dive & Internals goes inside the machine: the exact command write path, how projectors build read models off a message bus, the pairing with Event Sourcing (the append-only event log as source of truth), and the hard problems β€” the dual-write trap, projector idempotency, ordering, and rebuilding a projection with real numbers.

Sign in to MAANG.io

Use your Google or Microsoft account β€” no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.