Module 4: Cache — The Read-Heavy Path
1. Module introduction: the read rules
Description
This whole module hangs on a single number, and you already computed it in module 2: Enlace receives a hundred reads for every write. People create a short link once and visit it hundreds of times —they paste it in a tweet, in an email, in a chat of a thousand people— and each visit is a read. Translated to load: about ~40 writes per second against about ~4,000 reads per second. A system with that profile is called, in the jargon, read-heavy, and for a read-heavy system the most profitable tool that exists has a name: cache. This module is Enlace's cache, built piece by piece and with each number computed, not quoted.
Before diving in, it's worth fixing what a cache is without frills, because the rest of the module rests on this definition. A cache is a copy of the most-requested data, kept in a faster place and closer to whoever requests it, so you don't have to go looking for it in the slow place every time. In Enlace, the "slow place" is the database from module 3 —an index, a disk, a network in between, tens of milliseconds per query— and the "fast place" is RAM, typically a Redis, where a piece of data is read in about a millisecond. The cache doesn't replace the database: it's placed in front of it and catches most of the reads before they bother it.
Connection to the module: this lesson is the map. Here you don't implement the cache yet —that starts in lesson 3—, here you install why it exists and how its success is measured. Lesson 2 translates the 100:1 into real pressure on the database and into the RAM-vs-disk latency gap that makes it worth it. Lesson 3 gives you the concrete pattern, cache-aside, with its miss → database → populate flow. Lesson 4 answers what gets thrown out when the RAM fills up: the eviction policies, with LRU at the front. Lesson 5 is the numeric heart: the formula L = h·L_cache + (1−h)·L_db, run for various hit ratios. Lesson 6 explains why you shouldn't cache the 6 billion records —the 80/20 rule and the working set that fits in RAM—. Lesson 7 attacks the thorniest problem, TTL and invalidation. And lesson 8, the project, puts you to size the complete cache: memory, hit ratio, resulting latency.
A note on the method, because it sets the tone of the whole module: every number is computed or run, never quoted from memory. The average-latency table of lesson 5 you'll see come out of a real Python script. The working-set memory of lesson 6 comes from a multiplication you reproduce. The LRU simulation of lesson 4 runs and shows you what it evicts. A system design without numbers is an opinion; with numbers, it's a defensible decision. This module trains you to defend.
The warehouse and the shelf by the register
Think of it this way. You have a hardware store. In the back, in the warehouse, is all the inventory: ten thousand items, organized by aisle and shelf. It's your source of truth, you have everything, but it's far: going to look for something in the warehouse takes two minutes of walking among the shelves. Up front, by the register, you have a little shelf with the twenty products people ask for all day —batteries, tape, bulbs, the common screws—. That shelf doesn't fit the whole inventory, and it doesn't need to: it only has what sells constantly, and reaching it takes two seconds.
When a customer asks for something, what do you do? First you look at the shelf by the register. If it's there —and for the popular products, it almost always is—, you give it to them in two seconds and you didn't walk to the warehouse. Only when they ask for something rare, that isn't on the shelf, do you make the long walk to the warehouse, bring it to them… and, if it's one of the ones they start asking for often, you also leave it on the shelf by the register for next time. The warehouse still has everything; the shelf only has the hot stuff. And since most of what's asked for is popular, most of the time you save the walk.
That shelf by the register is a cache, and the analogy is almost literal. The warehouse is your database (db): it has the 6 billion records, it's the source of truth, and it's slow to reach. The shelf is your cache (cache): it has only the hot links, it lives in RAM, and it responds in an instant. "Look at the shelf first" is the cache-aside pattern of lesson 3. "Only the hot stuff fits on the shelf" is the working set of lesson 6. "What you throw off the shelf when it fills up" is the eviction of lesson 4. And the question that decides whether all this was worth it is a single one: what fraction of the time was the product already on the shelf? That fraction is called the hit ratio, and it's the number that governs the module.
It's worth spelling it out in full, because it's the idea that holds up the eight lessons:
A cache bets that a few pieces of data are requested much more than the rest. It keeps those few in a fast place, serves most of the reads from there, and only bothers the database when it misses. Its success is measured with a number —the hit ratio— from which the system's average latency comes directly.
Worked example: the number that justifies everything
Let's bring the idea down to numbers right now, because the 100:1 isn't decoration: it's what makes this module mandatory and not optional. Let's reproduce Enlace's read pressure with the arithmetic from module 2, without quoting anything from memory.
# read_pressure.py — Enlace's read pressure, recomputed
NEW_URLS_PER_MONTH = 100_000_000
SECONDS_PER_MONTH = 30 * 24 * 3600 # 2,592,000
qps_write = NEW_URLS_PER_MONTH / SECONDS_PER_MONTH
qps_read = qps_write * 100 # read:write ratio = 100:1
print(f"writes/s = {qps_write:.0f}")
print(f"reads/s = {qps_read:.0f}")
print(f"reads/day = {qps_read * 86_400:,.0f}")
The calculation is direct: in a month there are 30 × 24 × 3600 = 2,592,000 seconds; a hundred million new URLs spread over that time give about 39 writes per second; and a hundred reads per write give about 3,858 reads per second. We round to the guide's anchor numbers: ~40 writes/s and ~4,000 reads/s.
What to expect. With Python 3.14.0, python read_pressure.py prints:
writes/s = 39
reads/s = 3858
reads/day = 333,333,333
Pause on the last line: ~333 million reads a day. That's the load that, without a cache, falls entirely on a single database. Lesson 2 will show you what happens to that database when you hit it 4,000 times per second (nothing good) and why each of those reads costs ~50 ms if it has to touch the disk. Lesson 5 will show you what happens when you put a cache in front and catch 90% of those reads before they arrive: the database stops seeing 4,000 reads/s and goes to seeing 386/s —a number it can breathe with—, and the average latency the user feels drops from 50 ms to 5.90 ms. That's the deal this module offers, and you'll compute those two numbers yourself.
The module's map
Six topic layers, plus this map and the project. They're not in any order: they go from why to how to how much to what can go wrong, and they end by sizing Enlace's real cache.
| Lesson | What it installs | What you walk away with |
|---|---|---|
| 1. The read rules | The 100:1 ratio, what a cache is and where it lives, the map | Understand why a read-heavy system calls for a cache |
| 2. Why Enlace needs a cache | The pressure on the database, the RAM-vs-disk latency gap, Redis | Justify with numbers that a cache here isn't a luxury |
| 3. The cache-aside pattern | The miss → database → populate flow; the read path and the write path | Implement the cache in resolve without corrupting the source of truth |
| 4. Eviction and LRU | Why you evict, LRU vs LFU vs random, Redis's policies | Choose what to throw out when the RAM fills up, with judgment |
| 5. Hit ratio and average latency | The formula L = h·L_cache + (1−h)·L_db, run | Compute the system's latency from the hit ratio |
| 6. Working set and 80/20 | Why the working set fits in RAM, the memory calculation | Estimate how much RAM the cache really needs |
| 7. TTL and invalidation | Expiration, delete-on-write, stale data | Keep the cache from serving lies |
| 8. Project: size the cache | Everything together: memory, hit ratio, latency, policy | Deliver a defensible sizing |
Notice the shape of the arc. Lessons 1 and 2 are the why: without them, caching is cargo cult. Lesson 3 is the basic how —the pattern—. Lesson 4 is the how when the resource runs out. Lessons 5 and 6 are the how much —the two numbers that turn "let's add a cache" into "I need ~333 MB of RAM and I'll expect 5.9 ms of average latency"—. Lesson 7 is what can go wrong —the dark side of the cache, staleness—. And lesson 8 puts it all together in a real sizing. If someday you design the cache layer of any system —not just Enlace—, you'll walk this exact same arc.
The module's canonical identifiers
The guide's code is in English; the prose, in Spanish. These are the names you'll see over and over in the module, so they don't take you by surprise when they appear:
cache— the fast store in RAM (in practice, a Redis client). It hascache.get(key)andcache.set(key, value).db— the database from module 3, the source of truth. Slow to reach, has everything.hit_ratio(thehof the formula) — the fraction of reads the cache did have. A number between 0 and 1. It's the king of the module.L_cache— the latency of a read the cache did have (a hit). In our examples, 1 ms.L_db— the latency of a read that had to be looked up in the database (a miss). In our examples, 50 ms.L— the average latency the user feels, a mix of the previous two according to the hit ratio.short_code/long_url— the key and the value Enlace caches: given ashort_code, the cache stores itslong_url.
And the formula that is the module's checksum, the one you'll run in lesson 5 and apply in the project:
L = h · L_cache + (1 − h) · L_dbThe average latency is a weighted average: a fraction
hof the reads are fast hits (they costL_cache), and the remaining fraction(1 − h)are slow misses (they costL_db). Withh = 0.9,L_cache = 1 ms, andL_db = 50 ms, it gives0.9·1 + 0.1·50 = 5.9 ms. That 5.9 is an anchor number of the module: if your calculation doesn't reproduce it, something moved.
The boundary: what's taught here and what's next door
This module lives inside a guide that in turn lives in an ecosystem of sibling guides. It has a clear rule about what belongs to it and what doesn't, and it's worth keeping in mind from now so you don't expect from here something that belongs to another lesson or another guide:
| Topic | What's covered here | Where the full development lives |
|---|---|---|
| Database replicas (primary/replica, lag, reading from replicas) | Nothing: the cache and the replicas are different answers to the same read problem, and the replicas are the next step | Module 5 of this guide |
| Sharding and consistent hashing | Nothing | Module 5 of this guide |
| Asynchronous invalidation with queues / events (publishing a "link updated" event so several caches find out) | Only mentioned as a boundary in lesson 7 | event-driven-architecture-guide |
| Load balancing between app servers | Nothing | Module 6 of this guide |
| Circuit breaker / bulkhead when the cache or the database fail | Nothing | resilience-and-reliability-patterns-guide |
The mechanical rule to remember it: if the question is "how do I avoid going to the database on most reads?", it's this module. If it's "how do I make the database handle the writes and the reads that do arrive?", it's module 5 (replicas and sharding). And if it's "how do I tell ten different caches that a piece of data changed, asynchronously?", that's a message queue, and it lives in the event-driven architecture guide. A cache in front of a database —the shelf by the register— is what we build here; the rest are neighboring pieces that connect but aren't taught in this module.
Common mistakes
Believing the cache replaces the database. What happens: someone hears "the cache is faster" and concludes they should keep the data there and be done, or that the database "is unnecessary". Then the cache restarts, the RAM is wiped, and —if the source of truth was only in the cache— the data disappears. Why it happens: "fast" is confused with "reliable". RAM is volatile: a restart empties it. How to detect it: ask yourself "if the cache turns off right now, do I lose data?". If the answer is yes, you have the architecture backwards. How to fix it: the source of truth is always the database, which persists on disk. The cache is a disposable copy of the hot stuff: if it's wiped, it's repopulated from the database, and nothing is lost. The warehouse has everything; the shelf is replaceable.
Thinking that "adding a cache" is a decision without numbers. What happens: someone adds Redis "because a cache is needed" without computing how much memory it needs, what hit ratio they expect, or what latency they'll get. Weeks later the cache is either huge and wasted, or small and with a bad hit ratio, and nobody knows why. Why it happens: "cache" sounds like a magic solution that requires no analysis. How to detect it: if you can't say in a number how much RAM you requested and what hit ratio you expect, you cached blindly. How to fix it: this whole module. A cache is sized: working set × bytes per entry = memory (lesson 6), and hit ratio → latency with the formula (lesson 5). The project forces you to put those numbers before "turning on" anything.
Assuming every cache helps every system. What happens: someone learns about caches in a read-heavy case like Enlace and adds them to a system that writes as much as it reads, or where each read requests a different piece of data that never repeats. The hit ratio ends up on the floor, the cache saves almost nothing, and on top of that it adds complexity and invalidation. Why it happens: a tool is generalized beyond the terrain where it shines. How to detect it: look at the read:write ratio and the repetition of the accesses. If you write almost as much as you read, or if a piece of data never repeats, the cache has little to catch. How to fix it: a cache pays off when (a) you read much more than you write —Enlace, 100:1— and (b) a few pieces of data concentrate the accesses —the 80/20 rule of lesson 6—. Enlace meets both; before caching anything else, verify it also meets them.
Exercises
Exercise 1 — Recompute the read pressure. Without looking at the worked example, reproduce with pencil (or with Python) the three anchor numbers of this module from "100 million new URLs a month" and the 100:1 ratio: (a) writes per second, (b) reads per second, (c) reads per day. Use 30 days of 24 hours.
See solution
(a) Seconds in a month: 30 × 24 × 3600 = 2,592,000. Writes/s = 100,000,000 / 2,592,000 ≈ 38.6, which we round to the anchor ~40/s.
(b) With the 100:1 ratio, reads/s = 38.6 × 100 ≈ 3,858, the anchor ~4,000/s.
(c) Reads/day = 3,858 × 86,400 ≈ 333,333,333, that is ~333 million reads a day.
What matters isn't the exact decimal, but the order of magnitude and the thread: ~40 writes/s, ~4,000 reads/s, ~333 M reads/day. Those three numbers justify the whole module. Notice that the last figure —333 million daily reads on a single database— is the one that makes the cache mandatory and not optional: no single database serves that comfortably, so you have to catch most of them before they arrive.
Exercise 2 — Place each piece on its shelf. For each statement, say whether it describes Enlace's cache (cache) or the database (db), and why in one sentence. (a) "It has the 6 billion records, it's the source of truth." (b) "It lives in RAM and responds in ~1 ms." (c) "If it turns off, nothing is lost: it's repopulated." (d) "It persists on disk; if it turns off, the data is still there." (e) "It only stores the hot links, not all of them."
See solution
- (a) Database (
db). It has everything and is the source of truth: the warehouse with the ten thousand items. - (b) Cache (
cache). RAM and ~1 ms is exactlyL_cache: the shelf by the register. - (c) Cache (
cache). It's disposable precisely because it's a copy; its contents can always be reconstructed from the database. - (d) Database (
db). Persistence on disk = the source of truth that survives a restart. - (e) Cache (
cache). Only the hot stuff fits and it needs to fit; that subset is the working set of lesson 6.
The moral connects with the first common mistake: the database is the persistent and complete truth; the cache is a fast, partial, disposable copy of the most-requested. Confusing their roles —storing the truth only in the cache— is the mistake that makes data disappear on the next restart.
Exercise 3 — Does this system want a cache? For each system, decide whether a read cache like Enlace's would help it a lot, a little, or almost nothing, and explain why in one sentence looking at (a) the read:write ratio and (b) whether a few pieces of data concentrate the accesses. (i) A URL shortener (Enlace). (ii) A live chat where each message is written once and read once. (iii) The home page of a heavily visited newspaper. (iv) A random-number generator where each response is new and never repeats.
See solution
- (i) Shortener (Enlace): helps a lot. 100:1 ratio (reads much more than it writes) and the viral links concentrate the visits (80/20 rule). It meets both conditions: it's the ideal case.
- (ii) Live chat: almost nothing. Each message is read roughly once, so there's no repetition to catch, and it writes as much as it reads. A read cache would have a very low hit ratio: nothing to reuse.
- (iii) Newspaper home page: helps a lot. Millions of reads against a few updates a day (read-heavy) and everyone requests the same home page (extreme concentration). It's almost as good a case as Enlace —and in fact that's why newspapers cache their home page aggressively.
- (iv) Never-repeated random: nil. Each response is different and the same one is never requested again; the hit ratio would be 0 by definition. Caching here only adds wasted memory.
The rule that distills: a read cache pays off when you read much more than you write and a few pieces of data are requested over and over. Enlace and the home page meet both; the chat and the random one meet neither. Before caching any system, ask these two questions —they're the filter that separates a useful cache from a decorative one.
Summary and next step
In this lesson you installed the idea that holds up the module: Enlace is a read-heavy system —100 reads per write, ~4,000 reads/s, ~333 million reads a day on a single database— and for such a system the most profitable tool is a cache: a copy of the most-requested data, in a fast place (RAM, typically Redis) and close to the compute, that catches most of the reads before they bother the database. The analogy that anchors it: the shelf by the register versus the warehouse in the back.
You set the module's vocabulary —cache, db, hit_ratio, L_cache, L_db— and the formula that is its checksum: L = h·L_cache + (1−h)·L_db, which with h = 0.9 gives the 5.9 ms you'll reproduce in lesson 5. And you drew the boundary: here you cache, in module 5 you replicate and shard, and asynchronous invalidation with queues lives in the events guide.
Before moving on you should be able to: define what a cache is in your words (copy of the hot stuff, fast and close); explain why the database —and not the cache— is always the source of truth; recite the three anchor numbers (~40 writes/s, ~4,000 reads/s, ~333 M reads/day); and name the two conditions that make a cache pay off (you read more than you write, and few pieces of data concentrate the accesses).
What comes next is turning the 100:1 into concrete pressure. In lesson 2 you'll see what really happens to a database hit 4,000 times per second, why a read that touches the disk costs ~50 ms while one from RAM costs ~1 ms, and why that fifty-times gap is what makes the cache not a luxury but a necessity.
Resources
- Caching — System Design Primer — the general overview of why and how caching is done in large systems, with the same patterns we'll see (cache-aside, write-through, eviction). An excellent reference map to come back to after each lesson.
- Designing Data-Intensive Applications, Martin Kleppmann — Chapter 1 (reliability, scalability, maintainability) — the conceptual framework of why we measure latency and load with numbers, and why a read-heavy system is designed differently. It's the canonical reference of the whole guide; this chapter prepares the ground for the whole module.
- Redis — What is Redis? — the official introduction to the in-memory store we'll use as Enlace's
cache. It serves to understand what "the shelf by the register" is concretely: a key-value dictionary that lives in RAM and responds in submilliseconds.