What's Coming in Week 4#
Last week, you mastered messaging and async processing. You learned how to reliably move data between services, handle failures gracefully, and maintain audit trails.
This week, we tackle the most deceptively simple topic in distributed systems: caching.
Everyone knows: "Just add Redis, it'll be fast!"
Reality:
"There are only two hard things in Computer Science: cache invalidation and naming things."
— Phil Karlton
What makes caching hard
- When do you invalidate? Too early = wasted cache. Too late = stale data.
- What do you cache? Everything = memory explosion. Nothing = no benefit.
- How do you handle cache miss storms? 1 miss = 1 DB hit. 10K misses = DB crash.
- How do you stay consistent? User updates profile, sees old data. Confused.
Week 4 teaches you to cache strategically, not reflexively.
A cache hit is fast, but caching is hard: when to invalidate, what to cache, and how 10,000 simultaneous misses can crash a database. The week walks one request path through cache-aside patterns, invalidation ($99 cached while the database says $79), the thundering herd, push vs pull feeds and the CDN → gateway → Redis → database tiers. It ends with Week 3's flow: a price update goes through the outbox and Kafka to an invalidation consumer that deletes the Redis key and purges the CDN.
Weekly Goal#
Cache strategically, not reflexively. Understand invalidation, consistency, and thundering herds.
By the end of this week, you'll be able to:
- Choose the right caching pattern for any scenario
- Design invalidation strategies that actually work
- Prevent thundering herd without complex locking
- Build multi-tier caching architectures
- Know when NOT to cache (sometimes the answer is don't!)
Why caching is hard
Q1/2 You give product prices a very long TTL to maximize the hit ratio. According to the intro, what's the trade-off?
Answer & explanation
Answer: C
Invalidation timing cuts both ways: too early wastes the cache, too late serves stale data. A long TTL on changing data leans hard toward "too late." Memory blow-up comes from caching everything, not from how long each entry lives.
Q2/2 A very popular cache entry expires and 10,000 requests for it arrive at the same moment. What does the intro warn will happen?
Answer & explanation
Answer: A
Without protection, one miss is one database hit, so 10,000 simultaneous misses are 10,000 database hits: a cache miss storm (thundering herd). Having only one request fetch while the rest wait is a protection you have to build; Day 3 covers how.
The Week at a Glance#
- Day 1: Caching Patterns
- Cache-aside vs Read-through vs Write-through vs Write-behind
- When to use each? What are the trade-offs?
- System: E-commerce product pages
- Day 2: Invalidation Strategies
- TTL-based vs Event-driven vs Versioned keys
- The hardest problem in CS — how to actually solve it
- System: Product catalog with inventory + pricing
- Day 3: Thundering Herd
- What causes it? Locking, probabilistic expiration, coalescing
- Protecting your database from cache miss storms
- System: Homepage with 50K requests/second
- Day 4: Feed Caching
- Cache-per-user vs shared cache
- Push-on-write vs Pull-on-read — the celebrity problem
- System: Social media feed for 10M users
- Day 5: Multi-Tier Caching
- CDN → API Gateway → App → Database
- What belongs at each layer? How to invalidate across tiers?
- System: API serving mobile + web, auth + anonymous
- Capstone: Complete Caching Architecture
- Design caching for a high-traffic e-commerce platform
- Apply all 5 days to build a production-ready solution
Key Concepts You'll Master#
Day 1: Caching Patterns#
%% title: The four caching patterns
flowchart TD
subgraph CA["Cache-aside (Lazy Loading)"]
direction TB
A1["Application"]:::core --> C1@{ icon: "sd:cache", label: "Cache" }
C1 -->|"Hit?"| R1(["Return"]):::ok
A1 -->|"Miss?"| D1@{ icon: "sd:database", label: "Database" }
D1 --> W1["Write to Cache"]:::cache
W1 --> R1b(["Return"]):::ok
end
subgraph RT["Read-through"]
direction TB
A2["Application"]:::core --> C2@{ icon: "sd:cache", label: "Cache" }
C2 -->|"Hit?"| R2(["Return"]):::ok
C2 -->|"Miss? Cache fetches from DB"| D2@{ icon: "sd:database", label: "Database" }
end
subgraph WT["Write-through"]
direction TB
A3["Application"]:::core --> C3@{ icon: "sd:cache", label: "Cache" }
C3 -->|"Write to DB synchronously"| D3@{ icon: "sd:database", label: "Database" }
C3 -->|"Return after both complete"| R3(["Return"]):::ok
end
subgraph WB["Write-behind (Write-Back)"]
direction TB
A4["Application"]:::core --> C4@{ icon: "sd:cache", label: "Cache" }
C4 -->|"Return immediately"| R4(["Return"]):::ok
C4 -.->|"Async write to DB later"| D4@{ icon: "sd:database", label: "Database" }
end
CA ~~~ RT
RT ~~~ WT
WT ~~~ WB
class C1,C2,C3,C4 cache
class D1,D2,D3,D4 store- Cache-aside (Lazy Loading)
- Pros: Only caches what's needed, cache failure doesn't break app
- Cons: First request always slow, potential inconsistency
- Use when: Read-heavy, can tolerate stale data
- Read-through
- Pros: Simpler app logic, cache handles loading
- Cons: Cache becomes a dependency
- Use when: Want to simplify application code
- Write-through
- Pros: Cache always consistent with DB
- Cons: Write latency includes cache + DB
- Use when: Consistency is critical
- Write-behind (Write-Back)
- Pros: Lowest write latency, can batch writes
- Cons: Data loss risk if cache fails before DB write
- Use when: Performance critical, can accept some data loss risk
Day 2: Invalidation Strategies#
| Sale starts | Wait, flip it: sale ended | |
|---|---|---|
| Cache shows | $99 | $79 (old sale price) |
| Database | updates to $79 (sale!) | now $99 (sale ended) |
| Customer expects | $99 | $79 |
| Actually charged | $79 | $99 |
| Outcome | Customer confused (but happy?) | Customer angry, support ticket, refund |
INVALIDATION STRATEGIES
1. TIME-TO-LIVE (TTL) — cache.set("product:123", data, ttl=300) # 5 minutes
- ok — Simple to implement
- ok — Self-healing (bad data expires)
- no — Stale for up to TTL duration
- no — All items expire at same rate (inventory vs descriptions)
2. EVENT-DRIVEN — When product updated → Publish event → Invalidate cache
- ok — Near real-time consistency
- ok — Only invalidate what changed
- no — Requires event infrastructure
- no — What if event is lost?
3. VERSIONED KEYS — cache.set(f"product:123:v{version}", data)
- ok — Old and new can coexist
- ok — Atomic switchover
- no — More complex key management
- no — Storage overhead
4. HYBRID (Reality) — Event-driven + short TTL as safety net. Best of both worlds.
Day 3: Thundering Herd#
- Normal operation: Cache has homepage data · 50,000 requests/second served from cache · Database: relaxing, 0 load
- Cache expires: All 50,000 requests simultaneously hit database · Database: "I'm in danger" · Response time: 10ms → 10,000ms → timeout · Users: "Site is down!"
This is the THUNDERING HERD.
SOLUTIONS
1. MUTEX/LOCKING — First request: Acquires lock, fetches from DB, updates cache. Other requests: Wait for lock, then read from cache. Problem: All requests waiting → high latency
2. PROBABILISTIC EARLY EXPIRATION — Don't expire at exact time. Random chance of refresh before expiration. Some requests refresh early, preventing stampede.
remaining = expires_at - now() # seconds until expiry
if random() < 1.0 / max(remaining, 1): # more likely as expiry nears
refresh_cache()3. REQUEST COALESCING — Multiple requests for same key → single DB query. All waiters get the same result.
async def get_with_coalesce(key):
if key in pending_requests:
return await pending_requests[key]
# A Task (unlike a bare coroutine) can be awaited by many callers
task = asyncio.create_task(fetch_from_db(key))
pending_requests[key] = task
try:
return await task
finally:
del pending_requests[key]4. BACKGROUND REFRESH — Never let cache expire. Background job refreshes before TTL. Cache always warm.
Day 4: Feed Caching#
10 million users. Each has personalized feed. User posts → appears in all followers' feeds.
| APPROACH 1: PULL ON READ (Compute on request) | APPROACH 2: PUSH ON WRITE (Pre-compute feeds) | |
|---|---|---|
| Steps | User opens app: 1. Get user's following list · 2. Fetch recent posts from each followed user · 3. Merge, sort, return | User posts: 1. Get user's followers list · 2. Push post to each follower's feed cache |
| Pros | Storage efficient · Always fresh | Fast reads (just fetch cached feed) · Predictable read latency |
| Cons | Slow for users following many accounts · High compute at read time | Write amplification (1 post → 1M cache writes for celebrities) · Storage heavy |
THE CELEBRITY PROBLEM
A celebrity posts → 300 million followers → Push to 300 million feed caches? → That's 300 million writes for 1 post!
SOLUTION: HYBRID
- Regular users (< 10K followers): Push on write
- Celebrities (> 10K followers): Pull on read
When building feed:
- Fetch pre-computed feed (pushed posts)
- Merge with celebrity posts (pulled on demand)
- Return combined feed
Day 5: Multi-Tier Caching#
%% title: Caching layers
flowchart TD
U@{ icon: "sd:user", label: "User" } --> CDN@{ icon: "sd:cdn", label: "CDN (Edge)<br/>TTL: 60s, Static assets, Public API" }
CDN -->|"Cache miss"| GW@{ icon: "sd:gateway", label: "API Gateway<br/>Rate limiting, Auth cache" }
GW --> R@{ icon: "tech:redis", label: "Application Cache (Redis)<br/>TTL: 5-60min, Business objects" }
R -->|"Cache miss"| QC@{ icon: "sd:cache", label: "Database Cache<br/>Buffer pool: hot pages in memory" }
QC -->|"Cache miss"| DB@{ icon: "sd:database", label: "Database<br/>The source of truth" }
class U client
class CDN,GW edge
class R,QC cache
class DB store| Layer | Cache here | Don't |
|---|---|---|
| CDN | Static assets (images, CSS, JS) · Public API responses (product listings) | Personalized content · Authenticated endpoints (be careful!) |
| API Gateway | Auth token validation · Rate limit counters | Business data |
| Application (Redis) | Session data · User profiles · Product details · Computed aggregates | — |
| Database | Buffer pool (MySQL 8.0 removed its query cache; PostgreSQL has none) | Rely on this alone |
Systems You'll Design#
System 1: Product Catalog Cache (Days 1-2)#
Challenge
- 1 million products
- 100,000 requests/second
- Prices change daily
- Inventory changes every second
- Users must see accurate stock
Key decisions
- Different TTLs for different data?
- Event-driven for inventory?
- How to handle flash sales?
System 2: Homepage Cache (Day 3)#
Challenge
- 50,000 requests/second
- Personalized for some users
- Changes hourly (featured products)
- Cache miss = database overwhelmed
Key decisions
- How to prevent thundering herd?
- Cache per user or shared?
- Background refresh strategy?
System 3: Social Feed Cache (Day 4)#
Challenge
- 10 million users
- Each follows 0-1000 accounts
- Some accounts have millions of followers
- Feed must feel "real-time"
Key decisions
- Push vs pull vs hybrid?
- How to handle celebrities?
- Cache per user or compute on read?
System 4: Multi-Tier API Cache (Day 5)#
Challenge
- Mobile + Web clients
- Authenticated + Anonymous users
- CDN in front
- Multiple data types with different freshness needs
Key decisions
- What to cache at CDN vs App?
- How to handle authenticated caching?
- Cache invalidation across all tiers?
How Week 3 Connects to Week 4#
| Week 3: Messaging | Week 4: Caching | |
|---|---|---|
| Transactional Outbox (Guaranteed delivery) | → | Event-driven invalidation (Cache updates via events) |
| Backpressure (Handle overload) | → | Thundering herd protection (Prevent cache miss storms) |
| Dead Letter Queue (Handle failures) | → | Cache failure handling (What if Redis goes down?) |
| Kafka Consumer Groups (Multiple subscribers) | → | Cache invalidation consumers (Services listening for changes) |
EXAMPLE: Product Update Flow
- Admin updates product price (Week 2: Idempotency)
- Update written to DB + Outbox (Week 3: Transactional Outbox)
- Event published to Kafka (Week 3: Queue vs Stream)
- Cache Invalidation Consumer receives event (Week 3: Consumer Groups)
- Cache entry invalidated or updated (Week 4: Invalidation)
- CDN cache purged via API (Week 4: Multi-tier)
- Next request gets fresh data (Week 4: Cache-aside)
Prepare for Week 4#
Pre-Reading (Optional but Helpful)#
-
Redis Documentation — Commands: GET, SET, SETEX, DEL, KEYS https://redis.io/commands/
-
CDN Basics — How edge caching works Cloudflare or AWS CloudFront documentation
-
Phil Karlton's Quote — Why cache invalidation is hard https://martinfowler.com/bliki/TwoHardThings.html
Questions to Ponder#
Before starting Week 4, think about:
-
In your current systems, what's cached?
- How do you know when to invalidate?
- Have you ever served stale data to users?
-
What would happen if your cache went down?
- Would your database survive the load?
- How quickly could you recover?
-
How do you handle cache warming?
- After a deploy, is the cache cold?
- How long until performance is normal?
Week 4 Success Metrics#
By the end of Week 4, you should be able to:
- Explain cache-aside, read-through, write-through, and write-behind patterns
- Design invalidation strategies for different consistency requirements
- Prevent thundering herd with at least 3 different techniques
- Choose between push-on-write and pull-on-read for feed systems
- Design multi-tier caching with clear responsibilities per tier
- Know when NOT to cache (and explain why)
- Estimate cache hit ratios and memory requirements
- Handle cache failures gracefully
A Taste of What's Coming#
Here's a problem you'll solve in Day 3:
It's 12:00:00 AM on Black Friday. Your homepage cache TTL is 60 seconds. You have 100,000 users waiting to refresh.
- 12:00:00 — the cache expires.
- 12:00:01 — 100,000 requests hit your database.
- 12:00:02 — your database is on fire.
How do you prevent this? Day 3 will teach you:
- Probabilistic early expiration
- Request coalescing
- Background refresh
- Mutex with timeout
- Cache warming strategies
You'll implement each one and know when to use which.
Get Ready#
Week 4 is where performance engineering meets reliability engineering. Caching seems simple until you've been paged at 3 AM because a cache stampede took down your database.
After this week, you'll never think "just add Redis" again. You'll think:
- What's the invalidation strategy?
- What's the thundering herd protection?
- What's the failure mode?
- What's the consistency guarantee?
- Should we even cache this?
See you in Week 4.
Week 4 Preview — Caching: Beyond "Just Add Redis"
Coming up: Day 1 will start with caching patterns. We'll design a product catalog cache for an e-commerce platform, handling the trade-offs between freshness and performance.
this lesson in shorts · 1
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.
