Patterns
| Pattern | How | Fits |
|---|---|---|
| Cache-aside (lazy loading) | App reads cache; on miss reads DB and writes cache | Most read-heavy data; simple |
| Read-through | Cache layer/library loads from DB on miss | Same as cache-aside, logic centralised |
| Write-through | App writes DB and cache together | Data read right after writing |
| Write-behind | App writes cache; cache flushed to DB later | Very high write rates; risk of loss, use with care |
| Refresh-ahead | Refresh popular keys before they expire | Hot keys with expensive loads |
Cache-aside example
def get_product(pid):
key = f"product:{pid}"
cached = r.get(key)
if cached:
return json.loads(cached)
product = db.fetch_product(pid)
r.set(key, json.dumps(product), ex=3600 + random.randint(0, 300)) # TTL with jitter
return product
def update_product(pid, data):
db.update_product(pid, data)
r.delete(f"product:{pid}") # invalidate after the DB commit
TTL and invalidation rules
| Data | Approach |
|---|---|
| Reference data (catalog, config) | Longer TTL + delete on change |
| User sessions | TTL = session timeout; sliding expiry |
| Computed aggregates | Short TTL or refresh-ahead |
| Data that must never be stale | Don't cache, or write-through with strict invalidation |
Always set a TTL, even with explicit invalidation, so missed invalidations heal themselves.
Stampede protection
When a hot key expires, many requests hit the database at once.
| Technique | How |
|---|---|
| TTL jitter | Randomise expiry so keys don't expire together |
| Single-flight lock | First request sets SET lock:key 1 NX EX 10 and reloads; others wait briefly or serve stale |
| Stale-while-revalidate | Store a soft expiry inside the value; serve stale while one worker refreshes |
| Refresh-ahead | Background refresh for known hot keys |
Failure behaviour
- Timeouts on cache calls short (milliseconds), with fallback to the database.
- Circuit breaker so a Redis outage doesn't exhaust app threads.
- Database sized (or rate-limited) to survive a cold cache, or warm the cache on start.
- Don't store the only copy of important data in a cache-configured Redis (eviction).
Key design
- Namespaced keys:
service:entity:id. - Version in key for format changes:
product:v2:123. - Avoid huge values and huge collections (see the memory optimisation post).
Tip
Track cache hit ratio, database load and p95 latency together. A falling hit ratio after a release usually means a key format or TTL change, and it shows up as database load first.
Checklist
- Pattern chosen per data type
- TTLs with jitter on every cache key
- Invalidation after DB commits
- Stampede protection for hot keys
- Timeouts, fallbacks and circuit breakers
- Hit ratio and DB load monitored