The Problem Traditional CRUD Operations
A single CRUD model forces reads and writes to share one schema, one index set, one lock domain, and one storage engine, so every knob you turn to speed one path structurally slows the other — that coupling, not raw load, is why the two cannot be scaled independently.
Why one model actually blocks independent scaling
"Reads and writes are different" is true but shallow. The real problem is physical coupling. In a traditional CRUD design, a balance inquiry and a deposit both hit the same rows in the same table:
- The index set is shared. Writes want few indexes because every
INSERT/UPDATEmust also update every index on that table (write amplification + more WAL/redo). Reads want many indexes so varied query shapes stay fast. One table can only have one index set, so you are forced to pick a compromise that is wrong for at least one side. - The lock/MVCC domain is shared. A long analytical read holds a snapshot or shared locks on rows that writers are trying to mutate; the writers' row locks make readers wait. They contend on the exact same physical rows and versions.
- The read query cost is derived from the write schema. A normalized ledger (great for write integrity) means a balance must be computed by aggregating many rows. Adding read replicas duplicates that expensive aggregation onto every replica — you buy throughput with hardware but the per-query cost never drops, because the query shape is dictated by the write model.
So you can scale the machine (bigger box, more replicas), but you cannot scale the two workloads apart, because they are literally the same physical object with one set of tuning parameters.
Worked trace: a banking ledger under load
Concrete numbers make the coupling visible. Suppose a transactions table holds every posting: 1,000,000 accounts × ~5,000 postings each ≈ 5 billion rows. Balance is derived, not stored:
SELECT SUM(amount) FROM transactions WHERE account_id = ?;Live load: 12,000 balance inquiries/sec (mobile app auto-refresh) and 400 postings/sec (writes). Each inquiry aggregates ~5,000 rows, so the read path grinds through ~60 million rows/sec while the write path inserts only 400. Watch what happens to every attempted fix — each one speeds one path only by taxing the other, because both live in the same model:
| Change you make | Effect on READS | Effect on WRITES | Why it stays coupled |
|---|---|---|---|
Index on (account_id) | Locates the 5,000 rows fast, but still aggregates all of them per query | Every INSERT must maintain the index — extra WAL + write amplification | One table, one index set — the write pays for the read's index |
Covering index (account_id, amount) | Index-only scan, but still 5,000 entries summed | Larger index, more amplification per posting | Faster read is literally funded by slower write |
Materialized balance column on accounts | O(1) single-row lookup — huge win | Every posting now UPDATEs the same account row → row-lock contention; concurrent deposits serialize; MVCC version churn | Read and write now fight over the exact same row |
| Add read replicas | Spreads 12k qps across nodes — but each replica re-runs the same 5,000-row SUM; per-query latency unchanged | Primary ships every write to every replica; readers see stale balances (replication lag) | Replicas copy the write schema, so they copy its query cost |
No row in that table decouples the workloads. Each buys read speed with write pain (or the reverse) because there is one schema, one index set, one lock domain. The only way to scale the paths independently is to stop sharing the model: keep an append-only, write-optimized ledger and a separate, read-optimized store of precomputed balances, synced asynchronously. Splitting the model that way is CQRS — this problem is exactly the pressure that motivates it.
Pitfalls
- Hot-row contention from the "obvious" fix. Adding a materialized
balancecolumn feels free until a popular corporate account takes 50 concurrent postings — they all serialize on one row lock, and throughput collapses precisely where volume is highest. - Assuming reads >> writes automatically means CQRS. A read-heavy ratio is a signal, not a verdict. A cache or a read replica often solves it with a fraction of the complexity.
- Believing you "can't accept eventual consistency." Teams reject CQRS for stale reads while already running async read replicas — which are themselves eventually consistent. Be honest about the consistency you already tolerate.
- Analytical reads poisoning the OLTP box. A long reporting query on the shared model holds a snapshot that blocks vacuum/cleanup and inflates lock waits for live transactions — a classic "the dashboard took down checkout" incident.
- Write amplification sneaking in. Every index added to make reads fast multiplies write I/O; a table with 8 indexes turns one logical insert into nine physical writes.
- Splitting too early. Introducing separate read/write models on a low-traffic CRUD app pays the full complexity cost for a scaling problem you do not have yet.
When the single CRUD model is fine — and when to leave it
Single-model CRUD is a legitimate default, not a mistake. The engineering judgment is knowing which rung of the ladder your problem is actually on. There is a spectrum between "one model" and full CQRS; jump only as far as the pressure requires.
Keep one CRUD model when
- Read and write shapes are similar (you read roughly what you wrote), and the read query does not require expensive aggregation of the write schema.
- Load fits one well-tuned primary; a compromise index set serves both sides acceptably.
- Strong read-after-write consistency is a hard requirement and traffic does not justify an async pipeline.
The ladder of cheaper alternatives (try these before CQRS)
- Read replicas — gain: horizontal read throughput, tiny code change. Cost: replication lag (stale reads); does not lower per-query cost.
- Caching (e.g. cache the balance) — gain: O(1) hot reads. Cost: invalidation complexity, cache-stampede risk, still stale.
- Materialized view / summary table maintained by the DB — gain: precomputed reads without a full app-level split. Cost: refresh lag or trigger-driven write overhead.
Move to CQRS (separate read and write models) when
- The read and write workloads have genuinely divergent access patterns and volumes that a shared index set can no longer satisfy without hurting one side.
- Reads need shapes the normalized write model can't serve cheaply (search, dashboards, wildly different query surfaces).
- You must scale the two paths on independent hardware/throughput budgets.
What CQRS costs: two models to keep in sync, an async projection pipeline, eventual consistency on the read side, more moving infrastructure, and harder debugging. Choose the single model when a replica or cache closes the gap; prefer CQRS when read and write must evolve, scale, and be tuned as separate systems — and only then.
Takeaways
- The blocker isn't that reads and writes differ — it's that a single model makes them one physical object with one schema, one index set, and one lock domain, so tuning one side pessimizes the other.
- Adding replicas scales machines, not query cost: replicas duplicate the write schema and therefore duplicate its expensive read shape.
- The trade-off is unavoidable inside one model — every read speedup is a write tax and vice versa — which is the exact pressure CQRS relieves by splitting the model.
- Climb the ladder deliberately: replica → cache → materialized view → CQRS. Don't pay CQRS's consistency-and-complexity bill until a cheaper rung has clearly failed.
Re-authored/Deepened for this guide. Sources: Microsoft Azure Architecture Center, "CQRS pattern" and "Command and Query Responsibility Segregation"; Martin Fowler, "CQRS" and "ReportingDatabase"; Greg Young, "CQRS Documents"; Vaughn Vernon, Implementing Domain-Driven Design; Martin Kleppmann, Designing Data-Intensive Applications (derived data, materialized views, and the read/write coupling of a single storage engine).
🤖 Don't fully get this? Learn it with Claude
Stuck on The Problem Traditional CRUD Operations? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **The Problem Traditional CRUD Operations** (System Design) and want to truly understand it. Explain The Problem Traditional CRUD Operations from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **The Problem Traditional CRUD Operations** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **The Problem Traditional CRUD Operations** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **The Problem Traditional CRUD Operations** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.