Caching Strategies — When and How to Cache in Backend Systems
The Interview Question
What caching strategies do you know? How do you decide what to cache and when to invalidate?
Expert Answer
Caching stores frequently accessed data in a faster storage layer (typically in-memory) to reduce latency and database load. The three main patterns: Cache-aside (lazy loading) is the most common — the application checks the cache first; on a miss, it reads from the database, stores the result in cache, and returns it. The cache only contains data that's been requested. Write-through writes to both cache and database simultaneously — ensures consistency but adds write latency. Write-behind (write-back) writes to cache immediately and asynchronously writes to the database later — lowest latency but risks data loss if the cache crashes before persisting. Cache invalidation is famously one of the two hard problems in computer science. TTL (time-to-live) is the simplest approach: data expires after a set duration. Event-driven invalidation is more precise: when data changes, publish an event that triggers cache deletion. The golden rule: cache data that's read frequently, changes infrequently, and is expensive to compute or fetch.
Key Points to Hit in Your Answer
- Cache-aside (lazy): load into cache on first miss — most common pattern
- Write-through: write to cache + DB together — consistent but slower writes
- Write-behind: write to cache, async to DB — fast but risky
- TTL: simple but stale data until expiry
- Event-driven invalidation: precise but complex (pub/sub, change data capture)
- Cache stampede: many requests hit expired key simultaneously — use locking or pre-warming
- Redis vs Memcached: Redis has data structures, persistence, pub/sub; Memcached is simpler and faster for pure key-value
Code Example
// Cache-aside pattern (most common)
async function getUser(userId) {
// 1. Check cache
const cached = await redis.get(`user:${userId}`);
if (cached) return JSON.parse(cached); // cache hit
// 2. Cache miss — read from database
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// 3. Store in cache with TTL
await redis.set(`user:${userId}`, JSON.stringify(user), 'EX', 3600); // 1 hour
return user;
}
// Invalidate on update
async function updateUser(userId, data) {
await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
await redis.del(`user:${userId}`); // invalidate cache
}
// Cache stampede prevention with locking
async function getWithLock(key, fetchFn, ttl) {
let value = await redis.get(key);
if (value) return JSON.parse(value);
const lockKey = `lock:${key}`;
const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 10);
if (acquired) {
value = await fetchFn();
await redis.set(key, JSON.stringify(value), 'EX', ttl);
await redis.del(lockKey);
return value;
}
// Another process is fetching — wait and retry
await sleep(100);
return getWithLock(key, fetchFn, ttl);
}
What Interviewers Are Really Looking For
Know cache-aside cold — it's the pattern you'll use 90% of the time. The cache stampede problem (thundering herd) shows you think about production scenarios. Invalidation strategy is the real question: explain the tradeoff between TTL (simple, stale data) and event-driven (complex, fresh data). Mentioning Redis data structures (sorted sets for leaderboards, pub/sub for invalidation) shows practical Redis experience.
Practice This Question with AI Grading
Reading about interview questions is a start — but practicing with real-time AI feedback is how you actually get better. Goliath Prep grades your answers instantly and tells you exactly what you're missing.
Start Practicing Free →