Back to Event-Driven Architecture & CQRS
Q3Question 3 of 8

What are the CQRS trade-offs and when should you NOT use CQRS

Key Concepts
  • Benefits: independent scaling, optimized reads, simpler write logic, easy to add new query shapes without domain changes.
  • Costs: eventual consistency between write and read models, data duplication, more code and infrastructure.
  • Projection rebuild complexity: when a projection schema changes or a bug corrupts it, you must rebuild from source.
  • Do NOT use CQRS for simple CRUD where reads and writes share the same shape.
  • Do NOT use CQRS for low-traffic systems where the scaling argument disappears.
  • Do NOT use CQRS when every request needs strong read-after-write consistency with zero staleness.
  • CQRS is not all-or-nothing: apply it only to high-skew aggregates and keep the rest as plain CRUD.

Definition

CQRS buys independent scaling and fast tailored reads at the price of eventual consistency, duplicated data, and more moving parts, so skip it for simple, low-traffic, strongly-consistent CRUD.
  • Independent scaling – Read and write sides scale separately; in banking, reads outnumber writes so you scale read instances independently.
  • Optimized read models – Each screen is backed by a projection shaped exactly for it, no joins, no aggregate loading.
  • Eventual consistency – The read model updates after the write commits, so there is a brief staleness window.
  • Data duplication – The same information lives in both the normalized write model and denormalized read models.
  • Projection rebuild – When a projection schema changes or a bug corrupts it, you must rebuild it from the source of truth.

Running two separate counters at National Bank of India is powerful when the branch is huge and busy, but for a tiny village branch with five customers a day it is wasteful. You now need two sets of records that must be kept in sync, and for a brief moment after a transfer the display board might still show the old balance. If the branch is small, one combined counter is simpler and always perfectly up to date.

Understanding the Concepts

What are the benefits of CQRS?

The benefits are the reason anyone adopts CQRS. They matter most when read and write workloads diverge significantly.

JAVA
package com.bank.cqrs;

public class CqrsBenefits {
    public static void main(String[] args) {
        System.out.println("CQRS Benefits:");
        System.out.println("  1. Independent scaling: 10 query instances, 2 command instances");
        System.out.println("  2. Optimized reads: one indexed read, no 3-table join");
        System.out.println("  3. Simpler writes: aggregate only protects invariants");
        System.out.println("  4. New query shapes: add a projection, zero domain changes");
        System.out.println();
        System.out.println("At NBI: reads outnumber writes 50:1");
        System.out.println("  -> 10 read replicas, 2 write instances");
        System.out.println("  -> Statement screen: 1 read vs 3-table join before CQRS");
    }
}
TEXT
Expected output:
CQRS Benefits:
  1. Independent scaling: 10 query instances, 2 command instances
  2. Optimized reads: one indexed read, no 3-table join
  3. Simpler writes: aggregate only protects invariants
  4. New query shapes: add a projection, zero domain changes

At NBI: reads outnumber writes 50:1
  -> 10 read replicas, 2 write instances
  -> Statement screen: 1 read vs 3-table join before CQRS

What is eventual consistency and how do you handle it?

Because the read model updates after the write commits, there is a window where a just-written value is not visible on read. You must design the UI and APIs to tolerate this.

JAVA
package com.bank.cqrs;

public class EventualConsistencyHandling {
    public static void main(String[] args) {
        System.out.println("Problem: Rajesh transfers 50000, immediately views balance");
        System.out.println("  Write commits -> projection updates 100ms later");
        System.out.println("  If Rajesh reads in that 100ms, he sees OLD balance");
        System.out.println();
        System.out.println("Solutions:");
        System.out.println("  1. Return transfer result directly: 'New balance: 950000'");
        System.out.println("  2. Show pending state: 'Transfer processing...'");
        System.out.println("  3. Read-your-own-writes: route to write DB briefly");
        System.out.println("  4. Accept it: most dashboards tolerate 100ms lag");
    }
}
TEXT
Expected output:
Problem: Rajesh transfers 50000, immediately views balance
  Write commits -> projection updates 100ms later
  If Rajesh reads in that 100ms, he sees OLD balance

Solutions:
  1. Return transfer result directly: 'New balance: 950000'
  2. Show pending state: 'Transfer processing...'
  3. Read-your-own-writes: route to write DB briefly
  4. Accept it: most dashboards tolerate 100ms lag

What is the cost of data duplication?

The same information lives in the normalized write model and in one or more denormalized read models, which costs storage and introduces the possibility of drift if projections have bugs.

JAVA
package com.bank.cqrs;

import java.math.BigDecimal;

public class DataDuplication {
    // Write model: Account entity (normalized)
    //   account_id | holder_name | balance
    //   SAV2001    | Rajesh      | 950000.00

    // Read model 1: AccountSummaryView (denormalized for dashboard)
    //   account_id | holder_name | balance | last_txn | last_txn_time

    // Read model 2: AccountAnalyticsView (denormalized for reports)
    //   account_id | total_credits | total_debits | avg_balance_30d

    public static void main(String[] args) {
        System.out.println("Data duplication costs:");
        System.out.println("  Storage: balance stored in 3 places (write + 2 projections)");
        System.out.println("  Drift risk: a bug in projection logic -> read model is wrong");
        System.out.println("  Fix: idempotent projections + rebuild capability");
    }
}
TEXT
Expected output:
Data duplication costs:
  Storage: balance stored in 3 places (write + 2 projections)
  Drift risk: a bug in projection logic -> read model is wrong
  Fix: idempotent projections + rebuild capability

What is projection rebuild complexity?

When a projection schema changes or a bug corrupts it, you need to rebuild from the source of truth. With Event Sourcing you replay events; without it, you re-read the write model.

JAVA
package com.bank.cqrs;

public class ProjectionRebuild {
    public static void main(String[] args) {
        System.out.println("Projection rebuild strategies:");
        System.out.println("  With Event Sourcing:");
        System.out.println("    1. Truncate the projection table");
        System.out.println("    2. Replay all events from the event store");
        System.out.println("    3. New projection is built from scratch");
        System.out.println();
        System.out.println("  Without Event Sourcing (plain CQRS):");
        System.out.println("    1. Truncate the projection table");
        System.out.println("    2. Read all records from the write model");
        System.out.println("    3. Transform and load into the read model");
        System.out.println();
        System.out.println("  During rebuild, serve stale data or show 'rebuilding' status");
    }
}
TEXT
Expected output:
Projection rebuild strategies:
  With Event Sourcing:
    1. Truncate the projection table
    2. Replay all events from the event store
    3. New projection is built from scratch

  Without Event Sourcing (plain CQRS):
    1. Truncate the projection table
    2. Read all records from the write model
    3. Transform and load into the read model

  During rebuild, serve stale data or show 'rebuilding' status

When should you NOT use CQRS?

For simple CRUD, low traffic, strong consistency requirements, or when the team does not need independent scaling.

| Scenario | Should you use CQRS? | Why |
|---|---|---|
| Account statement (50:1 read:write, complex reads) | Yes | High skew, complex read models |
| Branch address reference data (edited monthly) | No | Simple CRUD, low traffic |
| ATM balance check (must show exact committed balance) | No | Strong read-after-write required |
| Loan calculator (stateless, no write model) | No | No write side at all |

JAVA
package com.bank.cqrs;

public class WhenNotToUseCqrs {
    public static void main(String[] args) {
        System.out.println("Do NOT use CQRS when:");
        System.out.println("  1. Simple CRUD: same shape for reads and writes");
        System.out.println("     Example: branch-address table edited 3 times/month");
        System.out.println("  2. Low traffic: scaling argument is zero overhead");
        System.out.println("  3. Strong consistency required: ATM must show EXACT balance");
        System.out.println("  4. No independent scaling needed: team does not need it");
        System.out.println();
        System.out.println("The mistake: adopting full event-sourced CQRS everywhere");
        System.out.println("The fix: apply CQRS only to high-skew aggregates");
    }
}
TEXT
Expected output:
Do NOT use CQRS when:
  1. Simple CRUD: same shape for reads and writes
     Example: branch-address table edited 3 times/month
  2. Low traffic: scaling argument is zero overhead
  3. Strong consistency required: ATM must show EXACT balance
  4. No independent scaling needed: team does not need it

The mistake: adopting full event-sourced CQRS everywhere
The fix: apply CQRS only to high-skew aggregates

How is CQRS not all-or-nothing?

You can apply CQRS to only the few aggregates that need it while the rest of the system stays plain CRUD. The lightest form shares one database.

JAVA
package com.bank.cqrs;

public class SelectiveCqrs {
    public static void main(String[] args) {
        System.out.println("Selective CQRS at NBI:");
        System.out.println("  Account Statements: CQRS (50:1 read:write skew)");
        System.out.println("  Transfer Processing: CQRS (complex write logic + reporting)");
        System.out.println("  Branch Addresses: Plain CRUD (tiny table, rare updates)");
        System.out.println("  KYC Documents: Plain CRUD (reads and writes same shape)");
        System.out.println();
        System.out.println("Result: complexity only where it pays for itself");
    }
}
TEXT
Expected output:
Selective CQRS at NBI:
  Account Statements: CQRS (50:1 read:write skew)
  Transfer Processing: CQRS (complex write logic + reporting)
  Branch Addresses: Plain CRUD (tiny table, rare updates)
  KYC Documents: Plain CRUD (reads and writes same shape)

Result: complexity only where it pays for itself

What is the more code and infrastructure cost?

Commands, handlers, events, projections, and two sets of repositories are more to build, test, and operate than a single CRUD model.

TEXT
Plain CRUD code count:
  1 Entity + 1 Repository + 1 Controller = 3 classes

CQRS code count:
  1 Command + 1 CommandHandler + 1 Aggregate + 1 WriteRepo
  + 1 Event + 1 Projection + 1 ReadEntity + 1 ReadRepo
  + 1 CommandController + 1 QueryController = 10 classes

Trade-off: 3x more code for the separation benefits.
Worth it only when those benefits matter.

What should a senior engineer know?

The senior decision is where on the CQRS spectrum to sit, not whether to adopt it everywhere or nowhere.

TEXT
Senior guidelines:
  - Lead with trade-offs, not with hype
  - Choose the lightest form that solves the problem
  - Monitor projection lag as a metric
  - Make projections idempotent and rebuildable
  - Apply CQRS only where read/write skew justifies it
  - Keep simple domains as CRUD and push back on over-engineering

Visual Overview

DIAGRAM
  CQRS Trade-off Decision:

  ┌─────────────────────────────────────────────┐
  │  Is read/write skew > 10:1?                 │
  │  Are read queries complex (multi-join)?     │
  │  Do you need independent scaling?           │
  └──────────┬──────────────────┬───────────────┘
             │ YES (any)        │ NO (all)
             ▼                  ▼
  ┌──────────────────┐   ┌──────────────────┐
  │  Consider CQRS   │   │  Stay with CRUD  │
  │                  │   │  Simpler, no     │
  │  Pick lightest   │   │  eventual        │
  │  form that works │   │  consistency     │
  └──────────────────┘   └──────────────────┘

  CQRS Spectrum:
  ┌────────┬────────────┬──────────────┬──────────────┐
  │ Light  │ Medium     │ Heavy        │ Full         │
  │ Same DB│ Same DB    │ Separate DBs │ Event Sourced│
  │ sync   │ async      │ async events │ + separate   │
  │ proj   │ projection │              │ read store   │
  └────────┴────────────┴──────────────┴──────────────┘

Key Takeaway

The honest way to talk about CQRS is to lead with the trade-offs rather than treating it as a silver bullet. The benefits are real: independent scaling of reads and writes, optimized read models with no joins, simpler write logic focused purely on invariants, and the freedom to add new query shapes just by building a new projection. In banking where reads outnumber writes by fifty to one, these benefits matter enormously.

But the costs are equally real. Eventual consistency means a client who just wrote and immediately reads may see stale data, so the UI must tolerate that. Data duplication between normalized and denormalized models costs storage and risks drift. More code and infrastructure: ten classes instead of three. And projection rebuild is operationally complex when schemas change or bugs corrupt the read model.

At National Bank of India, Pankaj made this distinction explicitly. The branch-address reference data is a tiny table edited a few times a month, so he kept it plain CRUD and pushed back when someone proposed CQRS. The account-statement feature served millions of reads with a fifty-to-one skew and no strong read-after-write requirement, so there CQRS was clearly worth it. CQRS is not all-or-nothing: apply it only to high-skew, high-traffic aggregates that tolerate staleness, and keep simple, low-traffic workloads as plain CRUD.

Remember one sentence: CQRS buys independent scaling and fast tailored reads at the cost of eventual consistency, duplicated data, and more moving parts, so apply it only to aggregates where read-write divergence justifies the complexity.

---

Share
Use ← → arrow keys to navigate