Caching is one of the highest-leverage performance techniques available, but picking the wrong strategy for a given data pattern causes stale data bugs that are genuinely hard to track down.
# Cache-aside (lazy loading): app checks cache first, falls back to DB, then populates cache
def get_user(user_id):
user = cache.get(f"user:{user_id}")
if user is None:
user = db.query_user(user_id)
cache.set(f"user:{user_id}", user, ttl=300)
return user
# Write-through: every write updates the cache AND the database together
def update_user(user_id, data):
db.update_user(user_id, data)
cache.set(f"user:{user_id}", data, ttl=300) # cache stays consistent immediately
# Write-behind (write-back): write to cache immediately, persist to DB asynchronously later
def update_user_fast(user_id, data):
cache.set(f"user:{user_id}", data)
queue.publish("persist_user", {"user_id": user_id, "data": data}) # eventual DB write
Choosing Based on Access Pattern
- Cache-aside — the most common default; simple, works well for read-heavy data that doesn’t change often
- Write-through — use when reads must always reflect the latest write immediately
- Write-behind — use for write-heavy workloads where a small eventual-consistency window is acceptable in exchange for much lower write latency
Cache invalidation — knowing exactly when to expire or update a cached value — remains the genuinely hard part regardless of which pattern you choose; a reasonable TTL as a safety net is worth having even alongside explicit invalidation logic.