Skip to main content

Command Palette

Search for a command to run...

Why CQRS Was Conceived: One System Cannot Serve Two Masters

Updated
14 min read
Why CQRS Was Conceived: One System Cannot Serve Two Masters

By now, we’ve seen both ends of the failure spectrum.

  • We tried to make read-optimized databases handle writes — and they crumbled under insert pressure.

  • Then we asked write-optimized systems to serve complex reads — and they silently broke under scan loads, joins, and lag.

Each failure looked different. But they shared one root cause:

One system was being asked to serve two fundamentally conflicting workloads.

One wanted transaction speed, isolation, and row-level precision.
The other wanted joins, projections, and scan-friendly aggregates.

At some point, someone stopped tuning indexes and retrying jobs long enough to say:

“What if we just gave each side its own database?”

And that wasn’t overengineering. That was survival.

That’s when CQRS stopped being an academic idea — and started being the only way forward.

This blog is about that split.

Not the buzzword.
Not the pattern.
But the structural decision to separate reads and writes — not because it looked clean, but because it was the only thing that kept systems alive.

Let’s break down what CQRS really is, how it works, and where it quietly saves teams who’ve already been through the fire.


What CQRS Actually Is

CQRS stands for Command Query Responsibility Segregation.
Sounds fancy. But at its core, it’s a simple idea:

Split the system into two separate models — one for handling commands (writes), and one for handling queries (reads).

Each side is allowed to optimize for what it’s supposed to do, without constantly being compromised by the other.


The Core Split

In a traditional system, you use one database and one data model for both reads and writes. Same tables, same indexes, same schema.

But as we’ve seen:

  • Write models want normalization, transactions, validation, and isolation

  • Read models want denormalization, projections, filtering, and fast lookups

Trying to optimize both in the same system leads to conflicting decisions. One wins, the other suffers.

CQRS says: split them.

AspectCommand Side (Writes)Query Side (Reads)
Optimized forBusiness logic, validation, consistencySpeed, projections, filtering
Schema shapeNormalizedDenormalized or flattened
Query patternsInserts, updates, deletesJoins, aggregates, lookups
ScalingScale with write throughputScale with query volume
StorageOLTP / row-basedOLAP / columnar, NoSQL, cache, etc.

What This Isn’t

CQRS is not:

  • Just “having two services”

  • Just “using a read replica”

  • Just “adding Redis in front of your DB”

  • A requirement to use Kafka or Event Sourcing

It’s a separation of data responsibilities, not a tech stack mandate.

And you don’t need microservices to do CQRS — you can do it inside a monolith if the model separation is clear.


How CQRS Actually Works

At a high level, CQRS introduces two separate models:

  • A Write Model responsible for handling commands — anything that changes system state

  • A Read Model built specifically for querying — optimized for performance, filters, and projections

These two models are often backed by different storage systems, updated at different rates, and shaped for different needs.

Let’s walk through how that actually plays out in production.


1️⃣ Step One: The Command (Write) Model

This is the source of truth. It’s where all business rules live, and where every state change originates.

You send in a command:

POST /checkout
{
  "userId": "abc123",
  "cartId": "xyz456"
}

The write model:

  • Validates input

  • Applies business rules (e.g. inventory check, promo validation)

  • Updates the core DB — typically normalized (e.g. PostgreSQL, DynamoDB)

  • Emits an event like:

      OrderPlaced(userId, orderId, timestamp)
    

The event is key. It decouples the read side from the write side — we’ll come back to this.


2️⃣ Step Two: The Event Propagation (The Sync Layer)

This is where things get interesting — and nuanced.

Once the command is processed and the event is emitted, the read model must be updated.

There are multiple ways to do this:

  • Event bus (Kafka, RabbitMQ, NATS)

  • CDC (Change Data Capture from the write DB)

  • Dual writes (but risky without idempotency)

  • Materialized view builders (ETL pipelines, background updaters)

Each event triggers a handler on the read side, which may:

  • Update a denormalized document in MongoDB

  • Recompute a cached projection in Redis

  • Write a flattened row into Elasticsearch

  • Append a new versioned snapshot into S3

This update doesn’t have to be immediate — and usually isn’t. That’s part of the tradeoff.


3️⃣ Step Three: The Read (Query) Model

Clients that want to query the system — for dashboards, search, filters, recommendations — hit the read model.

Here, performance and shape matter more than purity:

  • Data is often duplicated and denormalized

  • You might store precomputed aggregates

  • You might have multiple read models for different access patterns

This side is built to serve reads quickly and cheaply, without ever touching your core write DB.

For example:

  • A product catalog stored as one JSON doc per item

  • A leaderboard stored as a sorted list in Redis

  • A daily revenue summary precomputed per country

The write model might have 15 joins — the read model just fetches what’s already prepared.


🔁 Optional Flow Diagram (text version)

[Client] 
   │ 
   ├──> [Write API / Command]
   │        │
   │        ├──> [Write DB]
   │        │        (e.g. PostgreSQL, DynamoDB)
   │        └──> [Event Published]
   │                  (Kafka / CDC / Queue)
   ↓
[Read Model Updater]
   ↓
[Read DB]
   (e.g. Mongo, Redis, ClickHouse)
   ↑
[Client Queries Read API]

This separation has massive advantages — but it also comes with gotchas, which we’ll cover next.


When Should You Even Consider CQRS?

Not every app needs CQRS.
In fact, most don’t — at least not in the beginning.

If your app is still small, your reads and writes are light, and your schema is relatively stable, splitting your model might be premature. You’ll just be adding complexity without gaining much.

So when does CQRS actually make sense?


✅ 1. You’re Fighting Query vs. Transaction Conflicts

If you’re constantly running into:

  • Long-running reads blocking critical updates

  • Inserts getting slower due to read load

  • Teams arguing about which indexes serve “the real use case”

...you’re already halfway into CQRS territory. The split is overdue.


✅ 2. Your Read and Write Access Patterns Are Radically Different

Some tables are updated frequently but queried rarely.
Others are queried in complex ways but updated once a day.

If you find yourself twisting your schema to support both, you’ve outgrown a unified model.


✅ 3. Scaling Needs Are Diverging

Your writes are stable and few, but your reads are exploding — or vice versa.

This is where CQRS helps you:

  • Scale the read model aggressively with caching, replication, and denormalization

  • Keep the write model small, safe, and stable


✅ 4. Real-Time UX Is Clashing With Data Integrity

You want live updates, fast filters, instant search...
But you also want strict validation, ACID guarantees, and audit logs.

Trying to satisfy both in the same DB leads to compromise — either in UX or in data integrity. CQRS lets you serve both, cleanly.


✅ 5. You Already Have Event-Driven Boundaries

If you’re already emitting domain events (e.g., UserSignedUp, OrderPlaced), you’re positioned well for CQRS.

Those events can flow naturally into projection builders and read model updaters without forcing dual writes or schema hacks.


📌 In short:
You don’t reach for CQRS because you want to be clean.
You reach for it when your current model is breaking — and the breakage is coming from trying to serve two masters with one system.


Where CQRS Shines

Here are a few examples where CQRS fits naturally — not because it’s elegant, but because anything else breaks. (These aren’t the only places but you should get the gist)


1. E-Commerce Systems

You need:

  • Strong consistency on orders, payments, and inventory

  • Fast queries on product listings, filters, and category pages

The write model runs on Postgres with normalized order tables.
The read model uses Elasticsearch for product search, Redis for inventory counters, and MongoDB for denormalized product cards.

One side guarantees atomic order placement.
The other side powers 100K category page views per hour without touching the core DB.


2. Financial & Banking Systems

You want:

  • Immutable transaction logs with guaranteed order

  • Real-time account summaries, dashboards, and trend charts

The write model appends every transaction (debit, credit, transfer) to a ledger table.
The read model builds balance projections and timelines from that stream — often in a time-series DB or pre-aggregated cache.

Money is handled with strict writes.
Insights are served from a read model designed to scale.


3. Social Platforms & Content Feeds

Posting a comment or like should be fast and consistent.
But the feed UI needs:

  • Aggregated likes

  • Top comments

  • Paginated replies

  • Sorted and filtered data

You split:

  • Writes go to a transaction-safe DB (likes, posts, etc.)

  • Reads come from a flattened feed store, optimized for paging, scoring, and filtering

You stop trying to compute the feed live — you serve it from a read store that was built for exactly that access pattern.


4. Systems That Need Different Scaling Models

Writes may be few but critical. Reads may be many and noisy.

  • Write model stays on smaller, highly consistent DB nodes

  • Read model can scale horizontally, tolerate eventual consistency, and cache aggressively

You stop paying infra bills for use cases that don’t need strict consistency.


5. Search-Heavy Systems With Rich Filters

Search and filter-heavy UIs (like SaaS dashboards, analytics consoles, admin panels) often break when:

  • Filtering spans many columns

  • Aggregates are requested per time window, user, and status

  • Joins are needed across multiple business entities

Trying to serve that from a transactional schema becomes a recurring fight with the query planner.

CQRS gives you the freedom to flatten, pre-join, and reshape your data only for reads — without damaging your source-of-truth model.


In the next section, we’ll get into the Nuances and Gotchas — the parts most teams don’t talk about until they’re already in too deep.


Nuances and Gotchas of CQRS

CQRS solves real problems — but it also creates new ones.
Not because it’s broken, but because it shifts complexity from one part of the system to another.

If you’re going down this path, here’s what you need to account for.


1. Eventual Consistency Is Real (and Often Uncomfortable)

Your read model is not updated instantly.

  • You place an order — but the order dashboard shows it 5 seconds later

  • You update your profile — but the search filter still shows your old city

This isn’t a bug. It’s the cost of decoupling.

You need to:

  • Design your UI with graceful delays or placeholders

  • Avoid making business decisions on the read model

  • Be able to backfill or replay events when syncs fail

📌 If you're building systems where absolute freshness is a must (e.g., fraud detection, payment settlement), you’ll need to think hard about consistency guarantees.


2. Idempotency Is Mandatory

If your read model updates are triggered by events, those events:

  • Can be replayed

  • Can arrive out of order

  • Can be duplicated by queues or retry systems

This means your read model handlers must:

  • Be idempotent (same event processed twice = no problem)

  • Be version-aware (handle reordering or stale writes gracefully)

  • Avoid side effects during projection updates

📌 You can’t “just update the row” in a projection — you need to think like a stream consumer.


3. Debugging Gets Harder

Now that reads and writes are split:

  • You can’t just hit one DB to trace what happened

  • You can’t rely on single-transaction rollback

  • You need tooling to trace event → projection → read API

Without proper observability:

  • Debugging user complaints becomes slow

  • Data drift between models goes unnoticed

  • Engineers start pointing fingers across teams

📌 Add logging around projection builds, monitor lag, and build trace IDs across the event path.


4. Infrastructure Cost and Complexity Increases

You’re maintaining:

  • Two (or more) storage engines

  • Sync infrastructure (event bus, queues, CDC)

  • Error handling and replay logic

  • Multiple APIs (write and read layers)

You need to justify this added weight.

If your system doesn’t have clear workload separation or scale needs — you’ll hate the overhead.


5. Misusing the Read Model for Business Logic

This is one of the most common mistakes.

Teams start reading from the read model during validation:

  • “Let’s check the latest order count before allowing this coupon”

  • “Let’s block users if their read-side status is SUSPENDED

But remember: the read model is stale.
If you use it for decision-making, you will introduce race conditions.

📌 All critical logic must live on the command/write side.
The read model is just a view — never a source of truth.


Misuse Patterns: How Teams Get CQRS Wrong

CQRS is powerful — but only when it’s used for the right reasons and implemented with discipline.
Here are the most common mistakes teams make when they try to “CQRS” their system without fully understanding what that actually means.


❌ 1. Doing CQRS Just to Be Clean

Some teams split their read and write APIs too early — even when both hit the same database, and the same tables, and serve the same use case.

This isn't CQRS.
It's over-segmented CRUD.

If there’s no real difference in:

  • Data access patterns

  • Query shape

  • Performance pressure

...then you’ve just added ceremony for no gain.

📌 CQRS is a scaling decision, not a code organization technique.


❌ 2. Keeping the Same Schema on Both Sides

Another anti-pattern: teams split their models physically, but keep the same schema in both.

  • Same table structure

  • Same normalization

  • Same relational rules

  • Just duplicated in two different systems

This defeats the whole point.

The read side exists to serve queries efficiently, not to mirror your writes. Flatten, reshape, precompute, denormalize. If you’re not doing that, it’s not a read model — it’s a replica.

📌 If your read DB looks like your write DB, you’ve just created more infra to maintain the same bottleneck.


❌ 3. Expecting Real-Time Sync Without Accepting the Cost

Some teams implement CQRS and still expect:

  • Zero lag between writes and reads

  • Strong read-after-write guarantees

  • UI flows that depend on the read model being fresh every time

This is architectural schizophrenia.

You either get:

  • Strong consistency (single system, tight coupling)

  • Or high performance + separation (eventual consistency, lag tolerance)

Trying to get both just leads to flaky behavior and confused engineers.

📌 Design your UX and business flows to handle propagation delay — or don’t do CQRS yet.


❌ 4. Using the Read Model for Critical Write Decisions

This one keeps showing up:

  • “Let’s check the read model before allowing this transaction.”

  • “Let’s use the read model to enforce validation rules.”

It works — until the read model is 1 second stale, and you approve something that should’ve been blocked.

Never trust the read side for business enforcement logic.

📌 The write model is the source of truth. Everything else is a view.


❌ 5. Forcing CQRS Where the System Is Still Small

Some teams just want to be “future-ready.” So they start with CQRS from day one — separate models, event buses, multiple DBs — in an app that could fit in SQLite.

That’s a trap.

CQRS adds:

  • Infra

  • Failure points

  • Dev and ops complexity

If your system doesn’t need it yet, it will slow you down, not speed you up.

📌 Use it when the pain demands it — not because the blog post looked cool.


Closing Statement: The Architectural Divorce That Saved the System

By now, the reason CQRS exists should be clear:

  • Not because someone wanted to separate models for fun.

  • Not because it looks neat on diagrams.

  • Not because it’s a cool acronym.

But because, under real pressure, one system couldn’t serve two masters.

One side needed consistency, transaction safety, and isolation.
The other needed flexibility, projection speed, and scale.

Trying to satisfy both in the same model only led to:

  • Write throughput collapsing under read load

  • Read latency spiking due to lock contention

  • Schema changes breaking one path while trying to fix the other

CQRS isn’t a pattern you adopt to look senior.
It’s a structural decision you’re forced into once your system hits enough pain.


It’s not about making things elegant.
It’s about making things survivable.

CQRS gives you permission to stop compromising, to stop choosing which side suffers, and to let each part of your system be good at the one thing it was built for.

And once you split it, you rarely go back.


🛠 In the next post, we’ll shift from why to how — starting with the core question:

How do you keep the read model in sync with the write model — and live with eventual consistency without losing your mind?

We’ll go deep into event propagation, lag, replays, failure handling, and the hidden contracts that keep CQRS systems from drifting apart.

Look forward to The CQRS Sync Architecture: The Child That Came Out of the Divorce

Why CQRS Was Conceived

Part 5 of 7

Not another “what is CQRS” series. This one shows why it became necessary — through real-world failures, overloaded systems, and architectural pressure that forced teams to split reads and writes just to keep systems alive.

Up next

Why CQRS Was Conceived: When Write-Optimized Databases Are Asked to Read

What happens when you ask a sprinter to run a marathon while juggling spreadsheet