System Design ยท Lesson 4
Caching Strategies
Caching trades freshness and operational complexity for lower latency and reduced pressure on slower dependencies.
Cache-aside
The application checks the cache first, falls back to the database on a miss, then stores the result. This pattern is simple and works well for read-heavy data.
Cache a product detail endpoint
A catalog page receives thousands of reads but product records change only a few times per day.
async def get_product(product_id: str):
key = f"product:{product_id}"
cached = await redis.get(key)
if cached:
return json.loads(cached)
product = await db.products.get(product_id)
await redis.set(key, json.dumps(product), ex=300)
return productMost requests return from Redis in a few milliseconds instead of repeatedly hitting the database.
When a product changes, delete or refresh its cache key immediately rather than waiting for stale data to linger.
Avoid a cache stampede
If a popular key expires, hundreds of requests can miss simultaneously. One mitigation is a short lock so only one request rebuilds the value while others wait or serve slightly stale data.
What not to cache blindly
Highly personalized, permission-sensitive or rapidly changing data deserves extra care. A cache key that forgets tenant ID or authorization context can become a security bug.
Measure before adding layers
Cache because a measured bottleneck or latency requirement justifies it, not because every architecture diagram seems to contain Redis.