Caching Strategies
Distributed Systems & Scalability
Chapter 3 · Caching Strategies
Caching trades correctness risk for speed — every strategy in this chapter is a different way of drawing that trade. The famous line about cache invalidation being one of the two hard problems in computer science isn't a joke about difficulty in the abstract; this chapter builds a real, verified case of exactly what goes wrong.
Cache-Aside: Check the Cache First, Populate on Miss
PROD-1 (a genuine cache miss) took 10.37ms and incremented db.query_count to 1. Twenty subsequent reads of the identical key averaged 0.0005ms each — db.query_count never moved past 1. Cache-hit reads were roughly 19,566× faster than the original cache-miss read.
Write-Through vs. Write-Behind: Where the Real Tradeoff Lives
flush() ever runs, took 0.02ms total — roughly 12,145× faster, because nothing has touched the database yet.
write_behind.set('PROD-1', 100) and then checking database.get('PROD-1') before flush() runs returns None — the write genuinely never reached the database. cache.cache['PROD-1'] correctly shows 100 the whole time. Simulating a crash at exactly this point (the process ends, flush() never gets called) confirms the write is gone — not delayed, gone. Only once flush() actually runs does database.get('PROD-1') correctly return 100.
set() returns, the database and cache genuinely agree. Write-behind defers that cost — verified 12,145× faster — but during the deferral window, the only copy of the truth lives in memory, verified vanishing entirely on a simulated crash. This is the identical shape of tradeoff Distributed Systems & Scalability's own future CAP Theorem chapter formalizes: speed and immediate durability aren't both free at the same time.
Cache Invalidation: The Genuinely Hard Part
Reusing Software Architecture Fundamentals' own PricingEngine (Ch.7): what happens when a cached price outlives the data it was calculated from?
PROD-1 well-stocked (50 units), CachedPricingService.get_price() correctly caches and returns 100 (no scarcity pricing). Stock then genuinely drops to 5 — a real change that should trigger scarcity pricing. Querying the identical cache key again, without invalidating anything, still returns the stale 100 — while the correct price, computed fresh, is now 115.0. The customer would be quoted the wrong price, with no error, no warning, and nothing in the code path that looks broken.
cached_service.invalidate('PROD-1') — clearing every cached entry for that product — and querying again correctly returns 115.0, matching the true current price exactly.
CDN Basics
A Content Delivery Network applies cache-aside's own idea geographically: static content (images, scripts, stylesheets) gets cached at servers physically close to each visitor, instead of every request crossing however much distance separates the visitor from the origin server. Software Architecture Fundamentals Chapter 4 measured a real, non-zero cost for crossing a network boundary even on localhost — a CDN exists specifically to shrink that same kind of cost when the distance is a real geographic one, not a loopback address.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Write-behind's verified 12,145× speed gain, traded for a verified real data-loss window | Chapter 5's own CAP Theorem & Consistency Models — the same speed-vs-guarantee trade, formalized |
| A real, verified stale-price bug from a missing invalidation trigger | Software Architecture Fundamentals Chapter 6's own eventual-consistency finding — both are the same underlying risk (data that's technically present but no longer true) surfacing in different layers |
| Cache-aside's own 19,566× read speedup | Software Architecture Fundamentals Chapter 7's own ~18,111× testability speedup — a strikingly similar-shaped number, from the identical underlying idea: avoid real, slow I/O whenever it's safe to |
Hands-On Exercises
Extend this chapter's own CacheAsideRepository with a second key, 'PROD-2', and verify that a cache hit on 'PROD-1' doesn't accidentally also count as a hit for 'PROD-2' — confirm the first read of 'PROD-2' still triggers a genuine database query even after 'PROD-1' is fully cached.
Using this chapter's own WriteBehindCache, write two values for two different keys before ever calling flush(), then simulate a crash. Verify both writes are lost from the database, and verify calling flush() afterward (simulating a recovery that never happened) can't bring back data that was never in pending_writes to begin with.
This chapter's own CachedPricingService.invalidate() requires something else in the system to remember to call it whenever stock changes. Using this chapter's own verified stale-price bug, explain specifically what would have to change elsewhere in a real system (not in the cache itself) to make that invalidation call actually happen reliably.
Chapter 3 Quick Reference
- Cache-aside: check cache, miss falls through to the database and populates the cache — verified: ~19,566× faster on repeated reads, with
db.query_countnever incrementing past the first miss - Write-through: writes hit cache and database together, synchronously — always consistent, always pays the full write cost
- Write-behind: writes hit cache immediately, database later — verified ~12,145× faster, verified genuinely losing data on a simulated crash before
flush() - Invalidation, verified as the hard part: a real, wrong price (
100instead of the correct115.0) was served with zero errors until the cache was explicitly told to forget — the caching mechanism itself was never the problem - Next chapter: Database Scaling — replication, read replicas, and sharding