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.

Worked example

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 product
Read behavior

Most requests return from Redis in a few milliseconds instead of repeatedly hitting the database.

Update behavior

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.

Trade-off: a five-minute TTL lowers database load but permits stale data. The correct TTL depends on how harmful staleness is for that particular field.

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.