Implement cache-aside with Redis
Add a cache-aside layer to an existing Python function that calls a slow external API or database. You will implement get-on-hit, fetch-and-store-on-miss, TTL-based expiry, and explicit invalidation, then measure cache hit rate and latency across 100 calls with realistic key distribution.
Why this matters
Caching is the most impactful single optimisation for read-heavy systems. Cache-aside gives you full control: your application reads from the cache, fetches on miss, and invalidates when data changes. Getting the TTL wrong in either direction (too short, too long) is the most common caching bug in production; and you will reproduce both failure modes in this exercise.
Before you start
- Python with redis-py installed (pip install redis)
- Redis running locally (docker run -d -p 6379:6379 redis:alpine is the fastest way)
- A slow function to cache; a time.sleep(0.1) stub works fine, or use a real database query
Step-by-step guide
- 1
Set up the Redis connection
Create a redis.Redis client pointing at localhost:6379. Run client.ping(); it should return True. Write a helper that serialises Python objects to JSON for storage and deserialises on read.
- 2
Implement cache-aside read
Write a get_cached(key, fetch_fn, ttl=60) function: try client.get(key); if a hit, deserialise and return; if a miss, call fetch_fn(), store the result with client.setex(key, ttl, serialised), and return the result. This is the complete cache-aside pattern in under 10 lines.
- 3
Wrap a slow function
Apply get_cached to a function that simulates a 100ms call. Call it 10 times with the same key. Measure total elapsed time; the first call should be slow, calls 2-10 should be near-instant. Print hit/miss per call.
- 4
Test TTL expiry
Set TTL to 2 seconds. Call the function, wait 3 seconds, call again. The second call should be a miss even though you just fetched. This is the most common caching surprise: stale data after TTL expiry. Document the invariant for your team.
- 5
Implement explicit invalidation
Write an invalidate(key) function that runs client.delete(key). Call it after a simulated write operation. Verify the next read is a miss. The cache and the database are inconsistent between writes and the next TTL expiry unless you invalidate explicitly.
- 6
Measure cache hit rate
Run 100 calls with a Zipf key distribution: a few keys very frequently, many keys rarely. Print hit rate, average latency per hit, average latency per miss, and overall p95 latency. A well-tuned cache should hit 80%+ on typical read workloads.