What you'll learn
Quick Answer
A cache stores the result of expensive work so it need not be repeated. The difficulty is not storing things — it is knowing when the stored copy is out of date, and handling the moment many requests all miss at once.
Caches exist at every layer
Between a user and your database there are usually several, and knowing which one is serving stale data is most of debugging.
- Browser cache — files stored on the user's machine, controlled by response headers.
- CDN — copies at edge locations near users.
- Application cache — Redis or in-memory, holding computed results and query output.
- Database cache — the database keeping frequently-read pages in memory.
When a user reports seeing an old version after you deployed, the question is which of these is holding it. That is why content-hashed filenames matter: app.a3f9c1.js can be cached for a year safely because a change produces a different name, while app.js must revalidate.
The common patterns
Cache-aside is the one you will write most:
def get_student(id):
cached = cache.get(f"student:{id}")
if cached is not None:
return cached
student = db.query(id)
cache.set(f"student:{id}", student, ttl=300)
return student
Check the cache; on a miss, fetch and store. Simple, and the cache holds only what has been asked for.
Write-through updates the cache whenever the database is written, so the cache is never stale — at the cost of slower writes and caching data nobody reads.
Time-based expiry is the pragmatic default: accept that data may be up to N seconds old. Choosing that N is a product decision, not a technical one. A dashboard tolerating sixty seconds is easy; an account balance is not.
Invalidation, the genuinely hard part
The joke that cache invalidation is one of the two hard problems in computer science is repeated because it is accurate.
The difficulty is that the cache does not know when the underlying data changed. A student's marks are updated in the database; the cached copy is unaware and keeps serving the old value until it expires.
Options, in increasing order of correctness and effort:
- Short TTL. Accept staleness for a bounded period. Simple and usually enough.
- Delete on write. When updating the database, delete the cache key. Correct if you catch every write path — and the bug is always the path you forgot, such as an admin tool or a background job.
- Version the key. Include something that changes with the data, so a new version simply misses instead of needing deletion.
A useful rule: delete rather than update the cache on a write. Updating means computing the new value in two places, which will eventually disagree. Deleting means the next read recomputes it correctly.
The stampede, and other failure modes
Cache stampede. A popular key expires. A hundred simultaneous requests all miss, all query the database, all compute the same value. The database, comfortable a moment ago, is now handling a hundred identical expensive queries — and this happens precisely at peak traffic.
Mitigations: let one request recompute while others briefly serve the stale value; take a short lock so only one recomputes; or add jitter to TTLs so keys expire at different moments rather than together.
That last point matters more than it sounds. Warming many keys at once gives them the same expiry and guarantees a synchronised stampede later.
Cache penetration — repeated requests for something that does not exist miss every time and always hit the database. Cache the negative result too, briefly.
Unbounded growth — an in-memory cache with no eviction policy is a memory leak. Set a maximum size and an eviction policy such as least-recently-used.
When to cache, and when not to
Cache when the data is read far more often than written, when computing it is genuinely expensive, and when slightly stale is acceptable. A homepage product list is ideal.
Do not cache when correctness must be immediate — balances, stock counts during checkout, permissions — or when data is written as often as read, where you get invalidation cost without benefit.
And do not cache before measuring. A cache adds a whole class of bugs, and the slow part is frequently a missing database index rather than an absent cache. Adding an index is simpler, cheaper and has no staleness — see database indexing explained.
In interviews, mentioning invalidation and staleness unprompted signals real understanding. "I'd cache this with a sixty-second TTL because the data updates hourly and stale reads are acceptable here" is a much better answer than "add Redis".
