Someone says "let's use CQRS" in a design review, and just like with event-driven architecture, the nods around the table mean different things. One engineer pictures a second database fed by a message bus. Another pictures eventual consistency and a read replica that lags the writes by a few seconds. A third just wants to stop cramming query logic and business rules into the same bloated service class. All three are describing CQRS. The problem is that they are describing points on a spectrum that runs from a five-minute refactor to a distributed system with its own failure modes, and the meeting will not go well until everyone agrees on which point they mean.
Command Query Responsibility Segregation has one of the worst reputations-to-substance ratios in software. The idea underneath is small and sound. The reputation comes from teams reaching for the maximal implementation, complete with separate stores and asynchronous projections and eventual consistency, on a system that needed none of it. This letter is about the small idea, the spectrum it sits on, and how to tell where on that spectrum your actual problem lives.
It starts as a rule about methods
The root of CQRS is older than the acronym, and it is worth starting there because it makes the whole thing concrete. Bertrand Meyer's Command Query Separation says a method should be one of two things and never both. A command changes state and returns nothing. A query returns data and changes nothing. list.add(item) is a command. list.size() is a query. The rule you get for free is that a query can be called any number of times, in any order, and the world does not change. That property, that reads are safe to repeat, is the thing you spend the rest of your career leaning on without noticing.
Most code violates this constantly. The classic offender is the method that fetches a record and lazily creates it if missing, or the "get" that also bumps a last-accessed timestamp. Each one is a query wearing a command's side effects, and each one is a small trap for the next person who assumes reading is free.
CQRS is that rule, one level up
Greg Young, who named CQRS, described it as Command Query Separation applied not to a method but to the model. Instead of asking one method to be pure, you ask one object, and then a whole model, to serve either writes or reads but not both. You stop having a single Order class that both enforces "you cannot ship an order that is not paid" and also knows how to render itself as a row in an admin dashboard. You split it. One model exists to accept commands and protect invariants. A separate model exists to answer questions.
Here is the shape most teams start from, a single service that does everything:
class OrderService {
void placeOrder(PlaceOrder cmd) { /* validate, persist */ }
void cancelOrder(CancelOrder cmd) { /* check state, persist */ }
OrderSummary getSummary(String id) { /* join, shape for UI */ }
List<OrderRow> searchOrders(Filter f) { /* paginate, sort */ }
}And here is the same responsibility split down the middle:
// Write side: accepts commands, enforces rules, returns nothing useful
class OrderCommandHandler {
void handle(PlaceOrder cmd) {
Order order = Order.place(cmd); // domain object guards its own invariants
repository.save(order);
}
void handle(CancelOrder cmd) {
Order order = repository.load(cmd.orderId());
order.cancel(); // throws if state does not allow it
repository.save(order);
}
}
// Read side: answers questions, no domain logic, shaped for the screen
class OrderQueryService {
OrderSummary getSummary(String id) { /* read a flat projection */ }
List<OrderRow> search(Filter f) { /* read, paginate, sort */ }
}Nothing here says "second database." Nothing here says "message bus." The two handlers can read and write the same tables in the same schema. This is CQRS. It is the cheap version, and for a large number of systems it is the only version you ever need. What you bought is clarity: the write side is free to model a rich domain object with behavior and invariants, and the read side is free to be dumb, flat, and fast, because it never has to also be correct about business rules.
The spectrum, and why the price varies so much
The reason CQRS feels heavy is that people learn it from the diagrams at the far end of the spectrum. Lay out the whole range and the cost structure becomes obvious.
At the cheap end, you have two models in code, one database. Reads and writes hit the same tables. Zero new infrastructure, no consistency concerns, and you still get the design benefit of not overloading one class with two jobs.
One step out, still one database, but the read side queries denormalized views or read-optimized tables that the write side maintains inside the same transaction. Your queries get faster and simpler, and because the update happens in the write transaction, a read immediately after a write sees the new state. Still strongly consistent.
At the expensive end, two separate stores. The write model persists to one database, and a separate read store, often shaped completely differently, is kept up to date asynchronously by projecting the write model's changes onto it. This is the version in all the diagrams, and it is the version that buys independent scaling of reads and writes, a read store tuned to the query load, and the ability to rebuild read models from scratch. It is also the version that hands you eventual consistency as a permanent houseguest.
The mistake is not choosing the expensive end. The mistake is choosing it by default, because a conference talk drew it, rather than because your problem demanded it.
Where the model split actually pays
CQRS earns its keep when the write model and the read model genuinely want to be different shapes. That happens more often than you would guess and less often than the hype implies.
The write side, in a domain worth the trouble, is about invariants. An order must be paid before it ships. An account cannot be overdrawn past its limit. These rules live in aggregates, small consistency boundaries that load themselves, validate a change, and save. Aggregates are deliberately narrow. You load exactly the Order and its lines, not the customer's lifetime history, because the write side only needs enough state to decide whether this one command is legal.
The read side wants the opposite. A dashboard wants one row per order with the customer name, the total, the status, and the shipment tracking number already joined and denormalized. Forcing that query through the write model means loading rich aggregates and reshaping them in memory, which is slow and awkward. A dedicated read model, a flat table or a view that already looks like the screen, answers in one indexed lookup. When your reads outnumber your writes by two or three orders of magnitude, which is normal for most business systems, optimizing those two paths separately is not premature. It is the point.
The asymmetry is also operational. Reads and writes scale differently. A read store can be replicated widely and cached hard because stale reads are often acceptable. A write store has to serialize conflicting changes and cannot be casually replicated for writes. Separating them lets each scale on its own axis. That is the real argument for the expensive version, and it is a good argument exactly when the asymmetry is large and you have measured it.
Eventual consistency is a product decision, not a technical default
This is the part teams underestimate, and it is where CQRS projects go quietly wrong. The moment you move to a separate read store updated asynchronously, there is a window where a write has committed but the read model has not caught up. A user places an order and is bounced to their order list, and the order is not there yet, because the projection is a few hundred milliseconds behind. Technically the system is fine. To the user, the app just lost their order.
Greg Young has been blunt about this for years: eventual consistency in CQRS is a choice, not a requirement, and you should not adopt it reflexively. You can keep the read model updated inside the same transaction as the write and stay strongly consistent. You can update the UI optimistically from the command's own result, since the client already knows what it just did. You can scope the async projection to the read models that genuinely tolerate lag, like reporting and analytics, while keeping the user's own just-changed data on a synchronous path. The failure mode is treating "CQRS" and "eventual consistency everywhere" as the same word. They are not. The consistency model is a dial you set per read model based on what the product can stomach, and setting it to "eventual" for a screen that shows a user their own action is a product bug dressed as an architecture.
The three ways teams get it wrong
The first is applying it everywhere. Most screens in most applications are honest CRUD. A settings page, a tags list, an admin form. Splitting those into command and query models adds indirection and buys nothing, because there is no interesting write model to protect and no read shape worth optimizing. Both Martin Fowler and Microsoft's own guidance say the same thing in different words: CQRS is a pattern for specific bounded contexts inside a system, not a top-level architecture you impose on the whole thing. Use it where the model split pays and leave the rest as plain CRUD.
The second is welding it to event sourcing. CQRS and event sourcing show up together in so many talks that people assume one requires the other. It does not. You can do CQRS with ordinary tables on both sides. You can do event sourcing without ever splitting your read and write models. They compose well, because an event stream is a natural thing to project into read models, but adopting event sourcing to "do CQRS properly" means taking on an append-only log, versioned events, and replay tooling to solve a problem, the read and write split, that needed none of it. Two big ideas at once is how six-month projects become eighteen-month projects.
The third is discovering the staleness too late. A team builds the async projection pipeline because the diagram had one, ships it, and then finds out during user testing that the product cannot actually tolerate the lag on the screens that matter. Now they are retrofitting synchronous paths and read-your-own-writes hacks onto an architecture that assumed eventual consistency from the start. The fix is cheap if you decide the consistency model per read model up front, and expensive if you inherit it as a surprise.
When to actually reach for it
Reach for the cheap version, two models in code, almost freely. Whenever a service class is straining to be both a domain model and a query API, splitting the two responsibilities is a clean refactor with no infrastructure cost, and it makes both halves simpler. That alone justifies knowing the pattern.
Reach for the expensive version, separate stores and projections, when you can point at a specific bounded context with a large, measured read-to-write asymmetry, read and write models that genuinely want different shapes, and a clear-eyed decision about which read models can tolerate lag and which cannot. If you cannot answer the consistency question for each screen, you are not ready for the separate store yet.
The through-line is the same one that runs under most of these architecture patterns. The technique is not the hard part. Deciding how much of it your problem actually requires is the hard part, and CQRS punishes teams that skip that decision more than most.

