Back to blog
Series · Day 17
Solution Architecture in 30 Days
View all lessons →
CQRS

Day 17 — CQRS: the pattern for when "current state" has three different meanings

Nobody pitches CQRS by saying "we have an org problem." They pitch it as a performance fix — reads are maxed out, p99 is ugly, the database needs help. Nine times out of ten, that's not what's actually broken. What's broken is that three teams quietly stopped agreeing on what "current state" means, and one shared table is the only thing in the building still pretending they do.

The meeting where the model started breaking

Picture the design review: sales, fulfillment, and finance are all staring at the same `Order` table, and each one needs "current order state" to mean something different. Sales wants to know if the deal is still live. Fulfillment wants to know if the box has shipped. Finance wants to know if revenue can be recognized. For months, one shared read query served all three, with each team bolting on its own WHERE clause and its own private reading of the status flags. The breakage was never load. It was that the table had quietly turned into three incompatible APIs wearing one schema.

The tell: check what the ticket cites

When CQRS shows up in a proposal, read the justification before you read the diagram. Does it cite a load graph — read-to-write ratio, p99 under a specific query pattern, a database that's falling over? Or does it cite a Slack thread where two teams argued about what `status = 'confirmed'` actually means? In my experience it's almost always the second one, dressed up in the language of the first. "We need to scale reads" sounds like an engineering problem. It's a lot more fundable in a planning meeting than "nobody wants to own the fact that we disagree about what an order is."

What "current state" meant to each team, concretely

  • ▹Sales: current state = customer intent — did the buyer commit, is the deal still open, should the forecast count it this quarter
  • ▹Fulfillment: current state = physical reality — is the item picked, packed, on a truck, actually in the customer's hands
  • ▹Finance: current state = legally/contractually recognized truth — can revenue be booked, is the transaction reversible, what does the ledger say happened on what date

These aren't three views of one fact. They're three different facts that happen to travel together most of the time. A deal can be "confirmed" for sales — pure intent — while fulfillment hasn't touched it yet, while finance still can't recognize the revenue for another 30 days because of contract terms. One column, `status`, cannot honestly hold all three without one team's definition quietly steamrolling the other two.

The fracture point

The split stops being optional the day the shared model gives an answer that's correct for one consumer and flat-out wrong for another — not worded differently, wrong. In the order system, that day arrived when fulfillment marked an order `cancelled` after a warehouse scan failure, and sales' dashboard — reading that exact same field — reported a live deal as dead in the middle of a negotiation. Finance, reading the same field a third way, nearly reversed revenue that had never actually been recognized. One write, three teams, three separate kinds of damage. You don't patch that with a better enum. That's the schema confessing it was never one domain concept to begin with.

What CQRS is actually doing here

Separate read models per consumer aren't a cache layer, and they're not about shaving milliseconds off a query. They're a translation layer: each team gets a projection built from the same underlying events, encoding its own definition of "current state," with no way for one team's write path to leak into another team's read path. Sales gets an intent projection. Fulfillment gets a physical-state projection. Finance gets a ledger-grade projection with its own recognition rules. Nobody's edit can bleed into somebody else's view anymore, because there's no longer a shared mutable row for it to bleed through.

The write side doesn't go away

CQRS doesn't resolve the underlying disagreement — it can't, because the disagreement is about meaning, not mechanics. The command model still enforces exactly one set of invariants: inventory can't go negative, payment has to clear before fulfillment fires, an order can't be both cancelled and shipped. What CQRS does is take that one domain disagreement out of the shadows, where it was silently corrupting data, and turn it into an explicit, visible contract — three named projections, each owned, each documented, each wrong only if someone builds it wrong. Compare that to one table that's quietly wrong for whoever happens to ask the next question.

CQRS in the AI era: your "teams" might now be agents

This exact failure is showing up again, faster, with AI agents in the loop. Give a sales agent, a fulfillment agent, and a finance-reconciliation agent read access to the same order store, and each LLM will answer "what's the current state of this order" with total confidence — using whatever field it happened to be pointed at, with zero awareness that a sibling agent is reading that same field under a different definition. You don't even need a hallucination for this to go sideways; a correct read of an ambiguous shared model is plenty. Multi-agent systems raise the stakes because agents act on what they read — one can trigger a refund, a reship, or a journal entry off a "current state" that was never built to answer its question. The fix is the same one you'd reach for with human teams: give each agent its own projection, built off its own definition, from the same event stream. That's CQRS doing for an agent fleet exactly what it did for sales, fulfillment, and finance — not scaling throughput, just stopping conflicting consumers from corrupting each other's view of the truth.

The diagnostic takeaway for Day 17

Before you reach for CQRS, ask one question: who disagrees about what a read means? If sales, fulfillment, and finance — or their AI counterparts — all mean the same thing by "current state" and you just have too many reads hammering one table, you want a materialized view, a read replica, or a cache. Cheaper, simpler, no new contract to babysit. CQRS only earns its complexity when the disagreement is semantic, not volumetric.

  • ▹Load graph cited → probably a caching/scaling problem, not CQRS
  • ▹Slack argument about field meaning cited → probably CQRS, dressed as a performance ask
  • ▹Multiple consumers all agree on meaning → materialized view or replica
  • ▹Multiple consumers (human or AI) each act on their read with different assumptions → separate projections, one per definition of truth

The org lesson underneath the architecture

The real decision that triggered all of this wasn't technical. Nobody had explicitly decided who owns "current order state," so three teams each backfilled their own definition into a shared table without telling each other. CQRS didn't invent that disagreement — it made it legible in code: three named models instead of one table full of unstated assumptions. If you're the one proposing CQRS in a design review, you're really proposing to formalize a staffing and ownership decision that's already been made informally, and badly. Say that part out loud. It's usually the part that gets the proposal approved.

Flashcards
Check yourself

Extend your knowledge

  • ▹Map your own shared model: for one contested field (e.g. 'active user', 'current balance', 'order status'), write down each consuming team's actual definition in one sentence — you'll likely find the fracture before you need to build anything
  • ▹Read Greg Young's writing and talks on CQRS (he's the one who coined and popularized the term, building on Bertrand Meyer's earlier CQS principle) for the write/read separation mechanics this lesson assumes you already know
  • ▹If you're running multi-agent systems, audit which agents read from a shared store versus a dedicated projection — this is the same audit, just with agents as the disagreeing consumers
  • ▹Compare against Day 1-16 material on domain modeling and bounded contexts — CQRS here is really a bounded-context problem wearing a data-architecture costume
Test yourself on this lesson →

Discussion

Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.

Ask me anything about “CQRS” — trade-offs, decisions, or the story behind it.