Imagine you run a busy restaurant kitchen.
PostgreSQL -> chefs, waiters -> cache/Redis
The Write Side (Chefs) need things organized perfectly. Ingredients are in separate labeled containers.
Sauces here,
vegetables there,
meats in the fridge.
Everything has its own place so chefs can prep fast and never mix up orders.
This is your normalized Postgres schema. Clean, strict, no duplicates. Perfect for writing new orders.
OK:)
Now,
The Read Side (Waiters & Managers) need the opposite.
When a customer asks “Where is my order?”, the waiter does not want to check 7 different containers.
They want one single plate with everything ready to serve.
When the manager asks “How many biryanis did we sell yesterday?”, they do not want to open every container and count manually.
They want a pre-made summary sheet. This is your dashboard and reporting queries.
Right now your restaurant is using the same setup for both. Chefs are happy but waiters are running everywhere. Managers are counting ingredients at 9am and slowing down the entire kitchen.
You already added more shelves (indexes) and hired extra helpers (caching), but the fundamental problem remains. Writing needs structure. Reading needs speed. You have been pretending they are the same thing for two years.
So,
Technical Example:
Your orders service is a Postgres monolith doing 8K writes per minute and 40K reads per minute. Same tables. Same schema. Everything is grinding.
The write side needs a clean normalized schema with separate tables for orders, line items, payments, shipments, and addresses. Clean foreign keys, ACID compliance, no duplicated data. Beautiful for inserting new orders.
But the read side wants the exact opposite. Your dashboard joins 7 tables just to render one order card. Your reporting queries light up the CPU at 85% every morning at 9am.
You have already tuned indexes. You have already added caching. The fundamental problem is not hardware. The read model and the write model want different shapes of the same data, and you have been pretending they are the same thing for two years.
So here is the decision on the table:
A) Full CQRS with separate read/write models and eventual consistency
B) Add read replicas and point dashboards there
C) Denormalize the write schema itself so reads stop joining
D) Keep the schema and add GraphQL plus DataLoader to batch N+1 reads
First of all drop your answers from the given options (A, B, C, D).
FOLLOW
All four are real patterns you will see in production. Three of them dodge the actual problem. One of them is the senior engineer trap that looks correct for about 6 months until it does not.
Let me walk you through each one like I am sitting next to you.
Option A: Full CQRS ✅ (The Right Answer)
Think of this like having two separate notebooks. One notebook is for chefs (writes). It is perfectly organized with separate sections for ingredients, recipes, and plating. The other notebook is for waiters (reads). It has pre-written summaries like “Table 5 ordered biryani at 1pm” ready to serve instantly.
Writes go to your clean normalized Postgres. A background process projects that data into a flat denormalized read store like ElasticSearch or a separate read-optimized Postgres database. Dashboards and reports query only the flat store. Reads become instant because there are zero joins. Writes stay clean because nothing changed there.
The tradeoff is eventual consistency. The read store might lag by milliseconds to seconds. But for dashboards and reports, that is perfectly acceptable. Nobody expects real-time analytics down to the millisecond.
This solves the root cause. Read and write models finally get the shape they actually need.
Option B: Read Replicas ❌ (Moving Pain, Not Solving It)
You copy the same normalized schema to another server and point all reads there. Primary handles writes only. Sounds easy right?
But here is the thing. The replica still has the same 7-table joins. The same slow aggregation queries. The same CPU spike at 9am. You just moved the pain to a different machine. The underlying mismatch between read shape and write shape remains untouched.
Replicas help when your bottleneck is write contention. Your bottleneck is query complexity. Different problem.
Option C: Denormalize the Write Schema ⚠️ (The Senior Engineer Trap)
Flatten everything into one wide table. Line items embedded inside orders. Customer data duplicated everywhere. Reads become blazing fast single-table scans. No more joins. Dashboard loads in milliseconds.
Feels amazing for 6 months. Then reality hits.
A customer updates their address. Now you have to update it in 50 flattened rows across millions of records. A product name changes. Same nightmare. Data drifts silently. Anomalies creep in. Debugging becomes hell because you can no longer trust your source of truth.
This is the trap. It optimizes for today’s read performance at the cost of tomorrow’s data integrity. Senior engineers know this feels good now but hurts later. Junior engineers ship it and celebrate. Six months later everyone is firefighting.
Option D: GraphQL plus DataLoader ❌ (Optimizing the Wrong Layer)
You batch N+1 queries at the application layer. Instead of 100 round trips, you make 1. Network latency drops. Feels like progress.
But each batched query still hits the same 7-table join on the same overloaded database. You optimized the network call, not the actual bottleneck. The database is still crushing under complex joins at 40K reads per minute. DataLoader makes inefficient queries less wasteful. It does not make them efficient.
Why This Matters Right Now
You are at a 1:5 write-to-read ratio. That screams read-heavy workload. Your read model deserves its own optimized shape. Pretending one schema serves both masters is how monoliths die slowly and painfully.
Thank you, SUBBSCRIBEEEEEEEEEE :))))))))))))))))))))))) | OPEN TO WORK




Top comments (0)