How do you cache database reads and handle invalidation?
Take a moment.
Say your answer out loud or write it down first — retrieval is what makes it stick. Reveal when you're ready.
Short answer
Cache hot, slow-to-compute reads in Redis/Memcached keyed by query parameters with a TTL, using cache-aside (app checks cache, falls back to DB, populates cache). Invalidation is the hard part: expire by TTL for tolerant data, delete or update keys on write for fresh data, and version keys when schemas change. Cache only what profiling proves is hot.
Detailed explanation
Database CPU is usually the scarcest resource, and most workloads are skewed — a small fraction of rows serve most reads. Cache-aside (lazy loading) is the default: read-through path checks the cache first; on miss, query the DB and store the result with a TTL. Writes then choose: invalidate (delete the key so the next read repopulates — simple, brief extra load on miss) or update the cache inline (write-through, fresher but couples write path to cache availability). TTL bounds staleness: seconds for prices, minutes for profiles, hours for reference data — pick from product tolerance, not habit. Stampede protection matters: when a hot key expires, hundreds of requests can thunder onto the DB simultaneously — mitigate with probabilistic early refresh, request coalescing (single-flight), or a short stale-while-revalidate window serving slightly old data while one worker refreshes. Consistency limits: caches are eventually consistent by nature — never cache data the business needs strictly fresh (balances mid-transfer); keep transactional reads on the database. Failure mode: the cache is a performance layer, not storage — every read path must survive a total cache outage by falling back to the DB (possibly slower, never wrong). Key design (e.g., product:42:summary:v3) with versioning lets you roll formats without flush-all outages.
Example
-- Cache-aside pseudocode with stampede guard
-- key = f"product:{id}:summary:v2"
-- value = redis.get(key)
-- if value: return value
-- value = db.query("SELECT ... FROM products WHERE id = ?", id) -- single-flight this
-- redis.setex(key, ttl_seconds=300, value)
-- return value
-- On write: invalidate so the next read repopulates
-- UPDATE products SET price_cents = ... WHERE id = 42;
-- redis.delete(f"product:42:summary:v2")Interview tip
Name cache-aside first, then go straight to invalidation (TTL vs delete-on-write) and the thundering-herd guard. Closing with 'the app must survive total cache loss' shows you design for failure.
Common mistakes
- Caching everything instead of the proven-hot subset, adding complexity for no gain.
- Caching data that must be strictly consistent, like balances mid-transaction.
- Having no stampede protection, so one expiry DDoSes the database.