Cloud Native Patternsintermediate9 min

CQRS

Split the model that changes your data from the model that reads it, so each side can be shaped — and scaled — for the job it actually does.

Imagine a single counter at a busy government office that handles both filing new paperwork and answering questions about existing records. The filing clerk needs to check rules, stamp forms, and update the master ledger carefully — slow, deliberate work. The person who just wants to look something up needs a quick answer. Cramming both jobs into one counter, with one set of forms and one filing system, makes everyone miserable.

CQRS — Command Query Responsibility Segregation — is the software version of splitting that into two counters. One side handles the writes that change your data; the other side handles the reads that answer questions. Each gets its own model, optimized for what it actually does.

The problem

In most systems, one model and one data shape do everything: the same objects and the same tables serve both the code that changes data and the code that queries it. That feels tidy, but it forces constant compromises. The write path wants rich domain rules, validation, and a normalized shape that keeps every fact in exactly one place. The read path wants the opposite: data pre-joined and pre-shaped so a screen can be rendered from a single, cheap lookup.

The two sides also scale very differently. In a typical application, reads vastly outnumber writes — often by orders of magnitude. When both share one model and one datastore, you can't tune or scale the read side without dragging the write side along, and every query competes with every update for the same resources.

How it works

CQRS splits the system in two. The write side handles commands — requests that change state, like place order or update address. This is where the domain logic and validation live, working against a model shaped for correctness. The read side handles queries — requests that just return data. It serves those from a separate model shaped purely for fast reads, and each side can even have its own datastore.

Because the read model stands on its own, it can be denormalized and precomputed into materialized views or caches, so a query is a single cheap lookup instead of an expensive join. And because the two sides are independent, the read side can be scaled out on its own to absorb that much heavier read traffic without touching the write side.

Below, the same shop meets a sale-day spike twice: once with one model doing everything, once split with CQRS. Predict what the spike does to checkout, then flip the switch and decide what to scale.

Note

Command vs. query is the idea underneath the name. A command does something and changes state but ideally returns nothing; a query asks something and returns data but changes nothing. Keeping those two responsibilities strictly apart is exactly what the "responsibility segregation" in CQRS refers to.

Check yourself

In a CQRS system, where should the rule "an order can't exceed the customer's credit limit" be enforced?

Keeping the read side in sync

Once the read model lives apart from the write model, something has to keep it current. This is where CQRS pairs naturally with event sourcing: every change on the write side emits an event, and those events are replayed to update the read-side projections that queries are served from. Each read view becomes a precomputed answer that the write events keep refreshed.

You don't strictly need event sourcing — you can sync the read side with change-data-capture or background jobs — but the fit is clean. Either way, the write side stays the single source of truth, and the read side is a derived, query-optimized copy of it.

The trade-off: eventual consistency

The catch is that the read side is updated asynchronously. After a command succeeds, there's a brief window before the corresponding event propagates and the read model reflects the change — so the read side is eventually consistent with the write side rather than perfectly in step. This is the same propagation lag you see in replication, and you have to design the UX around it.

Step through it below: Alice places an order, then reloads her page while the event is still on its way to the read model. Predict what she sees before you look, then switch to a reload that waits for her own write.

You can't remove the lag, but you can keep it away from the one person who would notice it — the user who just made the change:

  • Show the change from the command's reply. The write model already said "OK, order #42", so the page can add #42 to the list itself instead of asking the read model straight away.
  • Wait for a version. The command returns the version it produced (v3). The next read asks for "at least v3", and the read side waits a moment, or answers "not yet", until it has caught up.
  • Read recent writes from the write side. For a few seconds after a change, that user's reads go to the write model, which is always current.

Other users may see the old list for a second, and that's usually fine: they didn't just place the order.

Check yourself

Users report that a comment they just posted is missing when the page reloads, then appears a second later. Which fix addresses the cause?

That lag, plus the extra moving parts — two models, separate datastores, the sync pipeline — is real complexity. For a simple CRUD screen where reads and writes use the same shape and there's no scaling pressure, CQRS adds cost without buying you much.

Watch out

CQRS is not all-or-nothing, and it's not free. Apply it only to the parts of a system that genuinely need it — a hot, read-heavy area with complex write rules — and leave the simple CRUD parts alone. Splitting a system that didn't need splitting just buys you two models, a sync pipeline, and consistency bugs.

When to use it

Reach for CQRS when the read and write workloads truly pull apart: complex domain logic on the write side, very different and demanding query needs on the read side, and a big imbalance between read and write volume. It shines in collaborative, high-traffic domains and pairs especially well with event sourcing and read-side caching.

Skip it when your data access is straightforward. If the same model serves reads and writes comfortably and you don't have scaling or query-shape pressure, a plain CRUD design is simpler, cheaper, and free of the eventual-consistency surprises that come with replicated, asynchronously-updated read models.

Key takeaways

  • CQRS stands for Command Query Responsibility Segregation: it separates the write side (commands) from the read side (queries) into two distinct models.
  • Forcing one model to handle both reads and writes leads to compromises, because complex write rules and fast, varied read needs pull the design in opposite directions.
  • The read side can be denormalized and precomputed into materialized views or caches, so queries are fast and reads scale independently of writes.
  • CQRS pairs naturally with event sourcing: write-side events update the read-side projections that serve queries.
  • The read side updates asynchronously, so it is eventually consistent with the write side — and that extra complexity isn't worth it for simple CRUD.

Keep going