A product page and a checkout button work with the same shop, but they need very different things from its data.
The page wants a quick answer: name, price, rating and availability. Checkout needs to decide whether an order is allowed and save the result correctly. We can handle both with an ordinary CRUD application. Sometimes, though, giving reads and changes separate models makes the design easier to work with.
That's the idea behind CQRS. Let's follow a shop from a straightforward database design to the point where that separation might earn its extra work.
Start with a design you can understand
CRUD stands for create, read, update and delete. In a small shop, one application and database can support all four operations with a shared model for products and orders.
The product page reads product rows. An admin updates a price. Checkout validates an order and saves it in a transaction. This design can be clear, reliable and fast; using CRUD doesn't prevent us from adding indexes, caches or carefully written queries.
Don't replace it just because the shop has more reads than writes. First measure what is slow and what makes the code difficult to change. A missing index is a poor reason to build another system.
Notice when the page and checkout want different shapes
Suppose the product page needs a name, thumbnail, review average and delivery estimate. The database keeps those facts in several places, so the page query has to assemble them.
Checkout has a different concern. It needs the current price, available stock and rules for accepting the order. A display-friendly object that combines everything isn't necessarily the right model for enforcing those rules.
We now have a reason to consider two representations: one organised around deciding and recording changes, another around answering the page's questions. That difference in purpose matters more than having two endpoints with different names.

Give the command and query different jobs
CQRS means Command Query Responsibility Segregation. A command asks the system to do something, such as place an order. A query asks for information, such as a product-page view.
An illustrative command might contain:
{"type":"PlaceOrder","product_id":42,"quantity":2,"request_id":"order-demo-17"}
The command handler must check the rules. It can reject unavailable stock or an invalid quantity; it shouldn't accept the browser's old display price as unquestionable truth.
The query can return the shape the screen needs:
{"product_id":42,"name":"Notebook","price_inr":249,"available":true}
That example view is convenient to render, but its available flag isn't a reservation. It describes what the read side knows. The command still decides whether the purchase can proceed when submitted.
First rule: queries don't place orders.
Separation doesn't require two databases
We can separate command and query code while keeping the same database. We can also use different models over shared storage. CQRS doesn't automatically require a message broker, event sourcing or asynchronous updates.
A more involved design gives the read side its own storage. After a successful change, an update process maintains a display-friendly view. That allows different indexing and scaling choices, but it creates a new question: how far behind can the view be?
Microsoft's CQRS guidance describes this range of designs. Start with the separation you actually need rather than assuming every implementation must include the most elaborate version.
An asynchronous read view can be briefly behind
Suppose the shop saves a price change from ₹249 to ₹299. Its separate product view still says ₹249 until the update arrives.
That's a normal possibility in an asynchronously maintained view. The interface must handle it honestly. At checkout, validate against the authoritative price and tell the customer about any change before asking them to confirm.
If the product page says an item is available, what should happen when checkout discovers the last one has already sold?
The purchase should fail or follow an explicitly supported alternative, rather than trust the stale view. Fast reads are useful because they help people browse; they don't remove the need to validate changes.
For workflows that need immediate feedback after a write, consider returning the accepted result directly or waiting for a known view version. Make that choice around the user experience and consistency requirement.
The read model becomes another thing to operate
A separate read store needs more than a process that usually copies updates. Ask how it handles:
- An update delivered twice.
- Updates arriving out of order.
- A stopped updater that leaves the page increasingly stale.
- A new view format that requires rebuilding old data.
Those are practical maintenance tasks, not reasons CQRS is inherently bad. They are reasons to choose it deliberately. Measure update lag, keep a recovery path, and test whether rebuilding a view can restore it correctly.
The gain may be worth it when complex write rules and demanding reads need different models. It may be unnecessary when one well-designed database and a few queries already meet the requirement.
Choose the smallest useful separation
For our shop, I would begin by keeping checkout rules clear and product queries focused. If assembling or scaling the views becomes a measured problem, we can introduce a dedicated read model with a freshness policy we understand.
CQRS gives us a way to express that difference between asking and changing. Its value comes from fitting those jobs better, not from turning a simple shop into a diagram with more boxes. Before adding the next box, be able to explain what becomes easier and who will maintain it when updates stop.
Try it yourself
A separate read model says a notebook costs ₹249, but the write model now has ₹299. Should PlaceOrder accept the display price? What should the user see?
Share this article with your friends
Know someone who would enjoy this? Send it their way.