Most of us start our career with one model for everything. The same Order class, the same table, the same object — we write to it, and we read from it. For 90% of applications this is perfectly fine, and honestly you should not change it. But once a system becomes big and read-heavy, this single model starts to feel tight, like a shirt that has shrunk after washing. CQRS is the pattern that loosens it. Let us go through it simply.
CQRS stands for Command Query Responsibility Segregation, which is a very heavy name for a fairly simple idea: use one model to change data, and a seperate model to read it. That’s it. Commands change state; queries return state; and the two no longer have to share the same shape.
The core idea
Think about it — writes and reads actually want very different things:
- A write cares about correctness. Is this order valid? Does the user have enough balance? Are all the business rules and invariants satisfied? For this you want a clean, normalised model with the domain logic in one place.
- A read cares about speed and shape. Give me exactly what this screen needs, fast, without joining seven tables every time. For this you want a denormalised model, shaped per view, maybe even sitting in a totally different store (Elasticsearch for search, Redis for a hot summary, and so on).
In the traditional single-model world, you are forcing both of these needs onto one poor model, and it does neither job well. CQRS simply says: let them go their own way. The write side handles commands and stays pure. The read side builds whatever views are convenient. When a command changes something, it emits an event, and the read side updates its models from that event. One stream of changes going in, many tailored views coming out.
The catch nobody tells you first: eventual consistency
Now here is the part which people conveniently skip in interviews, and then it bites them in production.
Beacuse the read side updates asynchronously from events, there is a small window — a few milliseconds, sometimes a few seconds — where the write has happened but the read model has not caught up yet. So a user saves their profile, the page reloads, and the old name is still showing. Technically everything is working correctly; it’s just eventually consistent.
This is not a bug to be fixed, it is a property to be designed around. Some practical ways:
- Read-your-own-writes: for the user’s own action, show the result optimistically from the command’s response, or read from the write side just for that one case.
- Make the rest tolerant: most reads (someone else’s dashboard, a search result) genuinely do not need to be millisecond-fresh. A small lag is totally okay there.
- Tell the user: a tiny “updating…” hint is far better than a confusing stale screen.
If your product truly cannot tolerate any staleness anywhere, then CQRS is probably not your friend, and that is fine.
A small code sketch
The shape in code is just two paths that do not share a model:
# WRITE side — a command, with rules
def handle(cmd: PlaceOrder):
order = Order.create(cmd.items, cmd.user_id)
order.validate() # domain rules live here
write_db.save(order)
events.publish(OrderPlaced(order.id, order.user_id, order.total))
# a projector updates the READ side from the event (async)
def on(event: OrderPlaced):
read_db.upsert_order_summary(
order_id=event.order_id,
user=event.user_id,
total=event.total,
status="placed",
) # denormalised, exactly what the "my orders" screen needs
# READ side — a query, no domain logic at all
def get_my_orders(user_id):
return read_db.order_summaries_for(user_id) # one fast lookupNotice the read query has zero business logic — it is a dumb, fast lookup into a model that was pre-shaped for it. That is really the whole benefit.
When to use it, and when to please not
I will be honest here, because CQRS is over-applied by people who read one blog and get excited (I was also guilty of this once).
Good fits:
- Read-heavy systems where reads and writes scale very differently.
- Complex domains where the write-side rules are rich, and you want to keep them clean.
- When you already need multiple read shapes — a search index, a reporting view, a cache — over the same data.
- Alongside Event Sourcing, where the two patterns fit together like hand in glove.
Please do not use it when:
- You have a simple CRUD app. One model with a few good indexes is simpler, and it will serve you well for years.
- Your team is small and the domain is still changing a lot. The extra moving parts will slow you down more than they help.
The real skill is not knowing CQRS — it is knowing when a plain boring model is actually the better engineering decision. Most of the time, it is.
Takeaways
- CQRS = one model to write, a separate model (or many) to read. Commands change state; queries return it.
- The write model stays clean and normalised; the read models are denormalised and shaped per screen. Events keep them in sync.
- The sync is usually async, so reads are eventually consistent — design for the small gap (read-your-own-writes, tolerant reads), don’t be caught out by it.
- Use it for read-heavy or complex-domain systems and alongside Event Sourcing — not for a simple CRUD app.
This is part of a small series on event-driven data patterns. Next, see Event Sourcing (its natural partner), Change Data Capture, the Outbox pattern, and for the bigger picture, the System Design Masterclass.










