Definition
- 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.
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");
}
}
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.
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");
}
}
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.
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");
}
}
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.
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");
}
}
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 |
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");
}
}
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.
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");
}
}
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.
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.
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
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.
---