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. The animation below shows the two paths: commands flowing through the write model on one side, queries served from the read model on the other.
- Write modelHandles commands and the domain rules that change state.
- Read modelA separate model shaped for fast queries — scales independently.
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.
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. A user who saves something and immediately reloads might, for a moment, see the old value. This is the same propagation lag you see in replication, and you have to design the UX around it.
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.
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.