Amazon's Product Page Problem
Amazon's product page shows: title, price, reviews, recommendations, inventory status, shipping estimates, and seller info. These come from 7+ different services. CQRS addresses this kind of read/write mismatch: the read path needs heavily denormalized, pre-joined data for speed, while the write path (placing an order) needs normalized, ACID-transacted data for correctness. Using the same model for both is a fundamental mismatch, and the fix is separating reads and writes completely rather than forcing one model to do both jobs.
CQRS Architecture
Traditional (Same Model for Read and Write):
[Client] --read/write--> [API] ---> [Same DB]
Problem: DB optimized for writes (normalized) is slow for reads.
Problem: DB optimized for reads (denormalized) loses write integrity.
CQRS (Separate Read and Write Paths):
WRITE SIDE (Commands): READ SIDE (Queries):
[Client] [Client]
| |
[Command API] [Query API]
| |
[Command Handler] [Query Handler]
| |
[Domain Model] [Read Model]
(validates business rules) (denormalized projections)
| |
[Write DB - PostgreSQL] [Read DB - Redis/Elastic]
(normalized, ACID) (optimized per query)
| ^
+---[Events via Kafka]-----------+
(sync read models asynchronously)Commands vs Queries
Commands (Write Side):
PlaceOrder(user_id, items, payment_info)
CancelSubscription(subscription_id, reason)
TransferMoney(from_account, to_account, amount)
Commands express INTENT. They are validated.
They can be rejected (insufficient funds).
They change state. They produce events.
Queries (Read Side):
GetOrderSummary(order_id) -> {status, items, total}
ListUserPurchases(user_id, page) -> [{order}...]
GetDashboardStats(date_range) -> {revenue, orders, users}
Queries are pure reads. They NEVER change state.
They read denormalized, pre-computed projections.
They can be cached aggressively.
They can hit a DIFFERENT database entirely.Synchronizing the Two Sides
Event-Driven Sync Flow:
1. Command: PlaceOrder(user=42, items=[...], total=$150)
2. Write DB: INSERT INTO orders (...) -- normalized
3. Publish: OrderPlaced event to Kafka
4. Event handlers update READ models:
Handler A: Update order-summary table (for GetOrderSummary)
Handler B: Update user-orders list (for ListUserPurchases)
Handler C: Update revenue dashboard (for GetDashboardStats)
Handler D: Update search index (for SearchOrders)
Each read model is a DIFFERENT shape of the same data,
optimized for its specific query pattern.
Trade-off: read models are EVENTUALLY consistent.
Write at T=0, read model updated at T=50ms to T=500ms.
For most UIs, this is imperceptible.When CQRS Is Worth It
Use CQRS when: Skip CQRS when:
------------------------------------ -----------------------
Read/write ratio > 10:1 Simple CRUD app
Reads need different DB (search, cache) Same model works
Multiple query shapes from same data Single list/detail view
Write model has complex business rules Write = read model
You need independent read/write scale Both scale together
You are already using event sourcing State is mutable
Complexity cost: 2 models, event sync, eventual consistency
Do NOT use CQRS for a simple blog or to-do app.Interview Tip
When discussing high-read systems, say: 'For a system with skewed read/write ratios like an e-commerce product page, I would use CQRS. The write side uses a normalized PostgreSQL database with ACID transactions for order placement. The read side uses denormalized projections in Redis for product pages and Elasticsearch for search. Events published via Kafka keep read models eventually consistent with writes. This lets me scale reads and writes independently and optimize each database for its access pattern.'
Key Takeaway
CQRS separates command (write) and query (read) models. The write side enforces business rules on normalized data. The read side serves pre-computed, denormalized projections. They stay in sync via events. Use CQRS when read and write workloads have fundamentally different requirements.
