system-design

Scaling Reads

Most systems are read-heavy, so this is where most scaling happens. The good news: reads are the easy direction — you can make copies of data freely.

Prerequisites: Caching, Replication Time to read: ~16 minutes


Why reads are the easy direction

🚨 Reads scale more easily than writes because you can duplicate data freely. A read doesn’t change anything, so you can serve it from a cache, a replica, a CDN, or a precomputed copy — as many copies as you want, all consistent-enough. Writes must be coordinated (only one source of truth); reads don’t.

📐 And most systems are read-heavy — often 100:1 or more. So scaling reads is where the leverage is, and the read/write ratio should be the first thing you compute, because it tells you reads are the problem to solve.


The read-scaling toolkit (in order of preference)

🚨 A rough ladder — apply the cheaper, higher-leverage ones first:

1. Cache — the biggest lever

🚨 The first and most impactful move. Serve repeated reads from memory instead of the database. Because reads repeat (the 80/20 rule — a small set of data serves most reads), a cache with a good hit rate removes most of the database’s read load.

95% cache hit rate → the database sees only 5% of read traffic → 20× reduction

Layers, cheapest/closest first (caching):

🚨 Hit rate is everything — above ~90% a cache is a huge win, below ~50% barely worth it. Ask “what hit rate do we expect?” first. And handle stampedes.

2. Read replicas — scale beyond one machine

Add read-only copies of the database; route reads to them, writes to the primary. Scales read throughput linearly with replicas.

✅ Scales reads horizontally, offloads the primary. ❌ 🚨 Replication lag — replicas are slightly stale, causing the read-your-writes bug (user posts, reads a replica, doesn’t see it). Handle by routing a user’s reads to the primary briefly after their write.

🎙️ “For read scaling I’d cache first — a 95% hit rate cuts database read load 20×. Then read replicas for what misses the cache, routing writes to the primary and reads to replicas — with the caveat of replication lag, so I’d read a user’s own recent writes from the primary.”

3. Denormalization — avoid the joins

Precompute and store data in the shape reads need, so a read is one lookup instead of a multi-table join. → NoSQL modeling, Relational modeling

✅ Fast reads (no join cost). ❌ Writes must update multiple copies; risk of inconsistency. Trade write complexity for read speed.

4. Precomputation / materialized views — do the work at write time

🚨 Move expensive work from read time (frequent) to write time (rare). Instead of computing an aggregate on every read, compute it once when the data changes and store the result. A materialized view or a precomputed feed.

📐 This is the fan-out-on-write idea: for a news feed read 100× per write, precompute each user’s feed at write time so reads are pure lookups. → CQRS

5. Search index / read-optimized store — CQRS

For complex read patterns the primary database serves poorly (search, analytics), maintain a separate read-optimized store (Elasticsearch, a read model), fed by CDC or events. → CQRS

6. Do less work / return less data


Read consistency: the trade-off you’re making

🚨 Every read-scaling technique trades freshness for speed/scale (caches are stale, replicas lag, denormalized copies can drift, precomputed data is a snapshot). This is fine for most data but must be a deliberate choice per data type:

🎙️ “These are all trading freshness for read scale, so I’d apply them per data type — cache and replicate the catalogue and feeds hard, but read inventory and balances from the primary where a stale read costs money.”


Where reads eventually hit a wall

Read scaling is easy until:


⚖️ Trade-offs

Technique Gain Cost
Cache 10-100× read offload Staleness; invalidation; failure mode
Read replicas Horizontal read scaling Replication lag; read-your-writes bug
Denormalization Fast reads, no joins Write complexity; drift risk
Precompute / materialized view Reads are lookups Write-time cost; snapshot staleness
CQRS / search index Reads served by the right store Sync pipeline; eventual consistency
Return less data Less transfer/compute

In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

1. Measure cache impact. Take a read-heavy endpoint hitting a database. Add a cache and measure database QPS and endpoint latency before and after at various hit rates. Watch a 95% hit rate cut database load ~20× — the biggest read-scaling lever, quantified.

2. Add a read replica and hit the lag bug. Route reads to a replica, writes to the primary. Post something and immediately read it from the replica — watch it not be there (replication lag). Then route own-recent-writes to the primary and watch it work.

3. Precompute vs compute-on-read. Build a feed two ways: compute it on each read (fan-out-on-read), and precompute it at write time (fan-out-on-write). Compare read latency and the read/write cost balance. See how precomputing trades cheap frequent reads for more expensive rare writes.

4. Denormalize a join. Take a query with a five-table join, denormalize the data into one read-optimized table, and compare read speed. Then update the underlying data and feel the write-side complexity of keeping the denormalized copy correct.


Check yourself

1. Why are reads easier to scale than writes? Because you can duplicate read data freely, while writes must be coordinated. A read doesn't change anything, so the same data can be served from many independent copies — caches, read replicas, CDNs, precomputed materializations — all serving simultaneously without conflicting, and you can add as many copies as you need for more read throughput. Writes are fundamentally different: there must be a single source of truth for each piece of data (or you get conflicts), so writes must be routed to and coordinated at that source, which doesn't parallelize the same way — scaling writes past one machine requires sharding, which sacrifices joins and transactions. This asymmetry, combined with the fact that most systems are heavily read-dominated (often 100:1), is why read scaling is where most of the practical work happens and why it's the "easy direction": caching and replication are cheap, effective, and don't require the painful trade-offs that write scaling does. It's also why computing the read/write ratio first is important — it usually confirms reads are the problem, pointing you at the easy tools.
2. Why is caching preferred over read replicas as the first read-scaling move? Because it's a bigger, cheaper lever. Caching serves reads from memory (nanoseconds to sub-millisecond) instead of the database, and because reads repeat heavily (the 80/20 rule — a small hot set serves most reads), a good hit rate removes most of the database's read load entirely: a 95% hit rate means the database sees only 5% of read traffic, a 20× reduction, from a single Redis instance or even in-process cache. Read replicas, by contrast, still involve full database queries (just on other machines), cost more (running additional database instances), and add replication lag and its consistency bugs. So caching gives a larger reduction in load for less infrastructure and often less complexity — you'd typically reach for it first, absorbing the bulk of reads, and add replicas as the *second* line for what still misses the cache and needs a real database query. The caveat is that caching's value depends entirely on the hit rate: above ~90% it's transformative, below ~50% it barely helps, so you ask "what hit rate do we expect?" before relying on it, and you handle cache stampedes and the cache's own failure mode (what happens when it's down and the database gets 100% of traffic).
3. What is the read-your-writes problem with read replicas, and how do you handle it? Read replicas lag behind the primary — writes go to the primary and take some time (milliseconds to seconds, more under load) to replicate. So if a user makes a write (posts a comment) and then immediately reads — and that read is routed to a replica that hasn't yet received the write — they don't see their own action, appearing as though it failed ("I posted a comment and it disappeared"). They may then retry and create a duplicate. It's one of the most common real-world consistency bugs from read replicas. The standard fixes: route a user's reads to the *primary* for a short window after they make a write (a session flag or timestamp, e.g. "read from primary for 5 seconds after this user's last write"); or track the write's replication log position and only serve that user from a replica that has caught up past it; or read user-modifiable data (their own posts, profile) from the primary while reading everything else from replicas. The general principle is read-your-writes consistency — users must see their own writes immediately even if others see them slightly later — and it's cheap to provide by being selective about which reads go to the primary.
4. What does "precompute at write time" mean and when is it worth it? It means doing expensive work once, when data changes (a write), and storing the result, so that reads become cheap lookups instead of expensive computations. Instead of computing an aggregate, assembling a feed, or running a complex query on every read, you compute it when the underlying data is written and save the answer — via a materialized view, a denormalized table, or fan-out-on-write. It's worth it when reads are much more frequent than writes, which is the common case: for a news feed read 100 times per write, computing the feed on every read (fan-out-on-read) means 100 expensive assemblies, while precomputing each follower's feed at write time means the write does the work once and the 100 reads are pure lookups — a huge net saving. The trade-off is that writes become more expensive (they now do the precomputation and may update many copies) and the precomputed data is a snapshot that can be briefly stale, but when the read/write ratio is high, trading cheap-frequent-reads for expensive-rare-writes is a large win. This is the essence of CQRS (separate read and write models) and the reason fan-out-on-write powers timelines at scale. It stops being worth it when writes are frequent relative to reads, or when the precomputation fans out to too many copies (a celebrity with 100M followers — hence hybrid approaches).
5. Why does every read-scaling technique involve a freshness trade-off, and how do you manage it? Because every technique works by serving reads from something *other* than the single authoritative source at the moment of the read: a cache holds a copy that may be stale, a read replica lags the primary, a denormalized table holds a copy that can drift from the source, a precomputed view is a snapshot from when it was last computed, and a search index is eventually consistent with the database. All of them gain speed and scale precisely by not going to the always-current source of truth, which means they can return data that's slightly out of date. You manage it by making the freshness decision *per data type*, deliberately, based on the cost of a stale read. Data that tolerates staleness — view counts, follower counts, product catalogues, recommendations, feeds — can be cached, replicated, denormalized, and precomputed aggressively, because a few seconds of staleness harms nothing. Data that needs freshness — an account balance at the moment of a transaction, inventory at checkout, a seat's availability during booking — must be read from the primary or a strongly-consistent store, because a stale read causes real harm (overselling, overdrawing, double-booking). So a well-designed read-scaling strategy isn't uniform: it applies the staleness-tolerant techniques hard to the data that can take it, while routing the correctness-critical reads to the authoritative source, paying for freshness only where it matters.

Further reading