Nguyen Le PhongNguyen Le Phong

CQRS: Command Query Responsibility Segregation

A practical explanation of CQRS: separating commands from queries, when read and write models deserve different shapes, and how to avoid turning a useful pattern into unnecessary architecture.

A product dashboard loads slowly after lunch. The same database table is trying to serve two very different jobs: protect every rule around changing an order, and also answer a wide reporting query that joins customer, payment, discount, shipment, and support data. Nobody set out to make the system awkward. One model simply became responsible for both careful writing and convenient reading.

CQRS stands for Command Query Responsibility Segregation. The name sounds heavy, but the core idea is small: separate the side of the system that changes state from the side that reads state. A command asks the system to do something: create an order, approve an invoice, cancel a booking. A query asks what is currently known: show the order summary, list overdue invoices, render the dashboard. Those two actions have different pressures, and sometimes they deserve different shapes.

On the command side, the model should protect business rules. It cares about validation, invariants, permissions, idempotency, transactions, and clear failure. It may not be optimized for showing every field the UI wants. That is fine. Its job is not to be convenient for every screen. Its job is to keep the system honest when something changes.

On the query side, the model can be shaped for reading. It can be denormalized, cached, indexed, precomputed, or stored in a form that matches a screen. An order details page may need customer name, payment status, shipment timeline, refund state, and support flags in one response. Instead of forcing the write model to become a reporting warehouse, a read model can hold that view directly and update when the underlying facts change.

This does not mean every CQRS system needs two databases, event sourcing, Kafka, or a diagram with too many arrows. At its simplest, CQRS can be separate command handlers and query handlers in the same codebase, using the same database. The important separation is conceptual first. Do not mix a write operation with hidden reporting logic. Do not make a query path accidentally change state. When the pressure grows, the physical separation can grow later.

CQRS becomes useful when the read and write sides are genuinely different. A product catalog may be read thousands of times for every one update. A finance workflow may have strict write rules but broad reporting needs. A customer support console may need a fast combined view of data owned by several services. In those cases, asking one model to be both strict and convenient can make it bad at both.

The cost is real. Once the read model is separate, the team must keep it up to date. If it is fed by events, events can be delayed, duplicated, or missed if the pipeline is weak. The UI may briefly show stale data. Engineers need to know where truth lives, how read models are rebuilt, how freshness is measured, and what happens when the projector fails. CQRS trades one kind of complexity for another. The trade only makes sense when the old complexity is already hurting.

The most common mistake is reaching for CQRS because it sounds senior. A simple CRUD screen with one table and moderate traffic probably does not need it. A small admin tool may be kinder with one model, one transaction, and clear tests. Architecture should remove pain, not create an identity. If nobody can name the read pain or write pain, CQRS is probably premature.

A practical adoption path is modest. First separate command functions from query functions. Then make command names explicit, like ApproveRefund or CancelSubscription, so they describe business intent. Next, create a read shape for one painful screen rather than the whole system. Add observability around update lag and rebuilds. Keep the write model boring and protected. This lets the team learn the pattern without betting the product on it.

CQRS also changes conversations with product and support. A read model may be eventually consistent, so the team should know whether a user can tolerate seeing an old status for a second. Some screens can. Some cannot. The checkout confirmation page may need stronger guarantees than an analytics dashboard. The architecture decision belongs partly to the business moment, not only to the code.

The quiet value of CQRS is that it gives each side a more honest job. Commands become about intention and rules. Queries become about visibility and usefulness. When those jobs are small, keep them together. When they pull against each other, separate them carefully.

The next time a single model feels stretched, ask which side is suffering. Are writes becoming risky because reporting fields keep leaking into the domain? Are reads becoming slow because the database is optimized for transactions? The answer may not be full CQRS. It may be one small read model, one clearer command boundary, and a team that finally stops asking one table to serve every purpose at once.

What did you think?