Events and messaging

4 min read ·

CQRS: When Reading Fast and Writing Correctly Need Different Designs

An order is confirmed, but the account screen still shows the old status. When separating reads and writes is worth it — and when it just adds more moving parts.

A customer places an order and immediately opens their purchase history. The order has been accepted, yet the screen shows the old state. An engineer might call this an expected delay in updating a projection. The customer might wonder whether the payment went through and the order disappeared.

That is a better starting point for discussing CQRS than drawing two databases on a slide. Do reads and writes actually have different needs, and what will the business pay to meet both?

Order changes on the left, a data core in the middle and prepared views serving users on the right
Business changes follow one path; user-facing views follow another. CQRS can still use a single database — this illustration represents separate responsibilities, not a mandatory physical deployment.

One model, two very different jobs

Accepting an order means enforcing business rules: is the item available, is the discount valid, does the user have permission, and is the proposed state change allowed? Getting these rules wrong can lose money. A command asks the system to change its state, and the system accepts or rejects that request.

Displaying order history requires a different shape of data: order number, total, status and delivery date. A management report may need aggregates instead. Asking every screen to reconstruct its answer from a complex write model can become expensive.

CQRS — Command Query Responsibility Segregation — separates the responsibilities of changing state and reading state. It does not require two physical databases. Separate handlers and models over one data store can be enough. Different databases are a later choice, not the definition of the pattern.

Where separation earns its place

Imagine an order service with relatively few writes but many reads from customer accounts, support tools and dashboards. Complex queries may contend with transaction processing. Dedicated read projections let the team optimise and, when needed, scale that path independently.

Sometimes the constraint is organisational rather than raw traffic. Adding a field to a report keeps touching code that enforces sensitive checkout rules. A read model gives those changes a safer place to live.

None of this means CQRS makes applications fast by default. If ordinary queries against a straightforward data model are already adequate, the extra handlers and projections may offer little value.

The bill arrives as data freshness

With separate read and write stores, changes must travel from one to the other, often through events or a change stream. A confirmed command may become visible to a query only after a delay.

That is eventual consistency. It can be entirely reasonable, but the product must decide what customers should see. Following an order submission, the interface might display the confirmed command result immediately, show a processing state or read critical details from the authoritative store.

The worst CQRS design is one where engineering accepts “the screen will catch up eventually” while the product has no answer for the confused customer. Inventory and payments tend to demand much tighter safeguards than summary reports.

Who rebuilds a broken projection?

A separate read model is derived data. Teams must know which events build it, how duplicates are recognised, how ordering problems are handled and how the view can be rebuilt after a processing bug.

Publishing an event separately from committing a database transaction creates another failure window: the write succeeds but the event is lost. A Transactional Outbox can address that gap; it does not remove the need for idempotent consumers and monitoring of projection lag.

Read latency is not the only useful metric. The age of the data matters. A fast screen showing yesterday's stock level may be worse than a slower one showing the truth.

Try a better query before redesigning everything

Before approving CQRS, I would ask for the actual bottleneck. Which queries interfere with writes? Would an index, a materialised view, a replica or a better schema be sufficient? Do the two paths really have different development or access requirements?

If the answer is simply that CQRS is a respected architecture pattern, a plain CRUD application is probably the better choice. Separation is justified when it removes a real constraint and the team is ready to own the data pipeline it introduces.

CQRS is valuable when it stops user-facing queries from distorting important business rules. But its flexibility is only worth buying when the consequences of stale data are understood.

Microsoft's Azure CQRS pattern guide covers both single-store and separate-store implementations.