Cache Strategies
Master the core caching patterns — cache-aside, read-through, write-through, and write-behind. Understand when and why to use each strategy for low-latency, high-throughput systems.
Table of Contents
The Big Picture — Why Caching Matters
A cache is a fast, temporary storage layer that sits between your application and a slower data source (usually a database). Instead of hitting the database on every request, you store frequently accessed data in the cache and serve it from there. The result: responses go from 50ms to 1ms, and your database handles 10x fewer queries.
The Restaurant Kitchen Analogy
A restaurant kitchen has two areas: the prep counter (cache) and the walk-in fridge (database). The most popular dishes — Caesar salad, fries, garlic bread — are prepped and ready on the counter. When a customer orders fries, the waiter grabs them from the counter instantly (cache hit, 1ms). When someone orders a rare dish, the chef goes to the walk-in fridge, gets the ingredients, cooks it, and puts an extra portion on the counter for next time (cache miss → fetch from DB → populate cache, 50ms). Without the prep counter, every single order would require a trip to the fridge. The kitchen would be slow, the fridge would be overwhelmed, and customers would leave.
🔥 Key Insight
Caching isn't just about speed — it's about protecting your database. A database that handles 10,000 QPS might collapse at 100,000 QPS. A cache in front of it absorbs 90% of reads, so the database only sees 10,000 QPS. Caching is a scaling strategy, not just a performance optimization.
Cache in the Architecture
Client
Sends request
App Server
Business logic
Cache
Redis / Memcached
Database
PostgreSQL / MySQL
Cache Hit vs Cache Miss
✅ Cache Hit
- Data found in cache
- Returned immediately (~1ms)
- Database is never touched
- Goal: 80-99% hit rate
❌ Cache Miss
- Data NOT in cache
- Must fetch from database (~50ms)
- Result is stored in cache for next time
- First request is slow, subsequent ones are fast
Hot vs Cold Data
🔥 Hot Data (Cache This)
- Accessed frequently (user sessions, popular products)
- Typically 5-20% of total data
- Generates 80-95% of all reads (Pareto principle)
- Perfect for caching — high hit rate
❄️ Cold Data (Don't Cache)
- Rarely accessed (old orders, archived logs)
- 80-95% of total data
- Caching it wastes memory for no benefit
- Let it live in the database
Without cache: All 100,000 reads/sec hit the database DB latency: 50ms average DB is overloaded, response times spike to 500ms+ With cache (90% hit rate): 90,000 reads/sec served from cache (1ms) 10,000 reads/sec hit the database (50ms) Average latency: 0.9 × 1ms + 0.1 × 50ms = 5.9ms DB load reduced by 90% Average latency reduced by 88% DB has headroom for writes and complex queries
💡 The 80/20 Rule of Caching
20% of your data serves 80% of your reads. Cache that 20% and you've eliminated 80% of your database load. You don't need to cache everything — just the hot data.
Cache-Aside (Lazy Loading)
Cache-aside is the most common caching strategy. The application manages the cache explicitly — it checks the cache first, falls back to the database on a miss, and populates the cache for next time. The cache is "lazy" because it only loads data when someone asks for it.
How It Works
Check the cache
Application asks the cache: 'Do you have user #42?' If yes (cache hit) → return immediately. Done.
On miss → fetch from database
Cache doesn't have it. Application queries the database: SELECT * FROM users WHERE id = 42. Gets the result.
Store in cache
Application writes the result to the cache with a TTL (e.g., 5 minutes): SET user:42 → {name: 'Alice', ...} EX 300.
Return response
Return the data to the client. Next request for user #42 will be a cache hit.
function getUser(userId) { // Step 1: Check cache const cached = cache.get("user:" + userId); if (cached) return cached; // Cache HIT → return immediately // Step 2: Cache MISS → fetch from DB const user = db.query("SELECT * FROM users WHERE id = ?", userId); // Step 3: Populate cache (TTL = 5 minutes) cache.set("user:" + userId, user, { ttl: 300 }); // Step 4: Return return user; } // On write: invalidate the cache function updateUser(userId, data) { db.query("UPDATE users SET ... WHERE id = ?", userId); cache.delete("user:" + userId); // Force next read to fetch fresh data }
Strengths
- ✅Simple to implement and reason about
- ✅Only caches data that's actually requested (no wasted memory)
- ✅Application has full control over cache logic
- ✅Works with any database and any cache (Redis, Memcached)
- ✅Most widely used strategy in production
Weaknesses
- ❌First request is always slow (cache miss → DB query)
- ❌Stale data: cache might serve outdated data until TTL expires
- ❌Cache stampede: if cache expires, many requests hit DB simultaneously
- ❌Application must manage both cache and DB (more code)
- ❌Inconsistency window between DB write and cache invalidation
🎯 Interview Insight
Cache-aside is the default answer in most interviews. When asked "how would you add caching?" — describe cache-aside first. It's the most common, most flexible, and easiest to reason about. Only move to other strategies when cache-aside's limitations are a problem.
Read-Through / Write-Through
Read-Through
With read-through, the cache itself is responsible for fetching data from the database on a miss. The application only talks to the cache — never directly to the database for reads. The cache acts as a transparent layer.
Cache-Aside (application manages): App → check cache → miss → App queries DB → App writes to cache Read-Through (cache manages): App → check cache → miss → Cache queries DB → Cache stores result App only ever talks to the cache. The cache handles DB fetching. From the application's perspective: const user = cache.get("user:42"); // If miss, the cache library fetches from DB automatically // App doesn't know or care whether it was a hit or miss
Read-Through strengths
- ✅Simpler application code (cache handles DB fetching)
- ✅Consistent read path (always through cache)
- ✅Cache library can optimize batching and prefetching
Read-Through weaknesses
- ❌Cache library must know how to query your DB
- ❌Less flexibility (cache controls the fetch logic)
- ❌First request still slow (cache miss → DB query)
Write-Through
With write-through, every write goes to the cache AND the database simultaneously. The write is only acknowledged after both succeed. This keeps the cache and database perfectly in sync — no stale data.
Application writes to cache
Application sends the write to the cache layer: 'Set user #42 name to Alicia.'
Cache writes to database
The cache synchronously writes the same data to the database. Both stores now have the same value.
Acknowledge to application
Only after BOTH cache and database confirm, the write is acknowledged. The application knows the data is consistent everywhere.
App: "Update user #42 name to Alicia" │ ├──→ Cache: SET user:42 = {name: "Alicia"} ✅ │ └──→ DB: UPDATE users SET name = 'Alicia' WHERE id = 42 ✅ │ └──→ App: "Write confirmed" (both succeeded) Result: Cache and DB are always in sync Any subsequent read from cache returns fresh data No stale data window Trade-off: Write latency = max(cache write, DB write) Typically 50-100ms (DB is the bottleneck) Every write is slower, but reads are always consistent
Write-Through strengths
- ✅Cache is always consistent with database
- ✅No stale data — reads always return the latest value
- ✅Simple mental model (write once, consistent everywhere)
- ✅Great for data that's read frequently after being written
Write-Through weaknesses
- ❌Higher write latency (must write to both cache and DB)
- ❌Writes data to cache that might never be read (wasted memory)
- ❌Cache fills up with infrequently accessed data
- ❌Not suitable for write-heavy workloads
🎯 Interview Insight
Use write-through when consistency between cache and database is critical — user settings, account balances, configuration data. Pair it with read-through for a fully transparent caching layer where the application never talks to the database directly.
Write-Behind (Async)
Write-behind (also called write-back) is the fastest write strategy. Writes go to the cache only, and the cache asynchronously flushes to the database later. The application gets an immediate acknowledgment without waiting for the database.
Application writes to cache
'Set user #42 name to Alicia.' The cache accepts the write and stores it in memory.
Acknowledge immediately
The cache responds: 'Write confirmed.' The application continues. The database hasn't been touched yet.
Async flush to database
In the background, the cache batches up pending writes and flushes them to the database — every 100ms, every 1000 writes, or on a schedule. The DB is updated eventually.
App: "Update user #42 name to Alicia" │ ├──→ Cache: SET user:42 = {name: "Alicia"} ✅ │ └──→ App: "Write confirmed" (instant, ~1ms) ... later (async, batched) ... Cache ──→ DB: UPDATE users SET name = 'Alicia' WHERE id = 42 Timeline: T=0ms: App writes to cache → confirmed T=0-1ms: App continues processing (no DB wait) T=100ms: Cache flushes batch to DB (background) Comparison: Write-through: write latency = ~50ms (wait for DB) Write-behind: write latency = ~1ms (cache only) → 50x faster writes
Strengths
- ✅Ultra-fast writes (~1ms, cache only)
- ✅Batching reduces DB write load (1000 writes → 1 batch)
- ✅Application never waits for the database
- ✅Great for write-heavy workloads (logging, analytics, counters)
Weaknesses
- ❌Data loss risk: if cache crashes before flushing, writes are lost
- ❌Inconsistency: DB is behind the cache until flush completes
- ❌Complex: must handle flush failures, retries, ordering
- ❌Not suitable for critical data (financial transactions)
⚠️ The Data Loss Risk
If the cache server crashes before flushing to the database, all pending writes are lost. For a logging system, losing a few seconds of logs is acceptable. For a banking system, losing a single transaction is catastrophic. Match the strategy to the data's importance.
End-to-End Scenario
Let's design the caching layer for an e-commerce product page — one of the most common interview scenarios.
🛒 E-Commerce Product Page
The page shows: product details, price, reviews, inventory count, and related products.
Traffic: 50,000 product page views per second. 100 product updates per second.
Read:Write ratio = 500:1 → heavily read-dominant.
Product Details → Cache-Aside
Product name, description, images change rarely. Use cache-aside with a 10-minute TTL. On product update, invalidate the cache key. 95%+ cache hit rate. Miss penalty: one DB query (~20ms).
Price → Write-Through
Price must always be accurate — showing a wrong price is a legal issue. Use write-through: every price update writes to cache AND database simultaneously. Reads always return the correct price. Higher write latency is acceptable (prices change rarely).
Reviews → Cache-Aside with longer TTL
Reviews change infrequently and slight staleness is fine. Cache-aside with 30-minute TTL. New review posted? Invalidate the cache. Users might see the old review count for up to 30 minutes — acceptable.
Inventory Count → Short TTL Cache-Aside
Inventory changes frequently (every purchase). Use cache-aside with a 10-second TTL. Slightly stale count is OK for display ('Only 3 left!'). The actual stock check at checkout uses the database directly (no cache) for accuracy.
View Count / Analytics → Write-Behind
Every page view increments a counter. At 50K views/sec, writing to DB on every view would kill it. Use write-behind: increment in Redis, flush to DB every 5 seconds. Losing a few view counts on a crash is acceptable.
Product Page Request Flow: Client → App Server → Redis Cache │ ├── product:42:details (cache-aside, TTL 10min) ├── product:42:price (write-through, always fresh) ├── product:42:reviews (cache-aside, TTL 30min) ├── product:42:stock (cache-aside, TTL 10sec) └── product:42:views (write-behind, flush every 5s) Cache hit (95% of requests): ~2ms total Cache miss: ~20-50ms (one DB query, then cached) DB load: reduced from 50,000 QPS to ~2,500 QPS (95% absorbed by cache) Result: fast page loads, healthy database, happy users
🔥 The Key Takeaway
Different data on the same page uses different caching strategies. Price needs write-through (consistency). Views need write-behind (performance). Product details need cache-aside (simplicity). There's no single "best" strategy — match the strategy to the data's requirements.
Trade-offs & Decision Making
| Strategy | Write Latency | Read Consistency | Data Loss Risk | Complexity | Best For |
|---|---|---|---|---|---|
| Cache-Aside | N/A (writes go to DB) | Stale until TTL/invalidation | None | Low | General purpose, most use cases |
| Read-Through | N/A (reads only) | Stale until TTL | None | Medium | Transparent caching layer |
| Write-Through | High (cache + DB sync) | Always fresh | None | Medium | Consistency-critical data |
| Write-Behind | Very low (cache only) | Fresh in cache, stale in DB | Yes (cache crash) | High | Write-heavy, loss-tolerant |
Consistency vs Performance
| Dimension | High Consistency | High Performance |
|---|---|---|
| Strategy | Write-through | Write-behind |
| Write latency | 50-100ms (wait for DB) | 1ms (cache only) |
| Stale reads | Never | Possible (DB lags behind cache) |
| Data loss risk | None | Yes (unflushed writes) |
| Use when | Price, balance, inventory | Counters, logs, analytics |
Freshness vs Speed
| TTL | Freshness | Cache Hit Rate | DB Load |
|---|---|---|---|
| No cache | Always fresh | 0% | Maximum |
| 10 seconds | Very fresh | ~70% | Low |
| 5 minutes | Moderately fresh | ~90% | Very low |
| 1 hour | Possibly stale | ~98% | Minimal |
| No expiry | Stale until invalidated | ~99% | Near zero |
🎯 Decision Framework
Ask: (1) Can this data be stale? If no → write-through. If yes → cache-aside with appropriate TTL. (2) Is this write-heavy? If yes and loss is acceptable → write-behind. (3) How stale is acceptable? Seconds → short TTL. Minutes → longer TTL. Never → write-through or invalidate on write.
Interview Questions
Q:What is cache-aside and when would you use it?
A: Cache-aside (lazy loading) is a pattern where the application checks the cache first, falls back to the database on a miss, and populates the cache for subsequent requests. Use it as the default strategy for most read-heavy workloads — user profiles, product details, configuration data. It's simple, flexible, and only caches data that's actually requested. The trade-off: first request is always slow (cache miss), and data can be stale until the TTL expires or the cache is explicitly invalidated.
Q:When would you use write-behind instead of write-through?
A: Write-behind when write performance matters more than durability. Examples: page view counters (50K increments/sec — can't hit DB for each), analytics events, logging. The writes go to cache only (~1ms) and flush to DB asynchronously in batches. Trade-off: if the cache crashes before flushing, those writes are lost. Write-through when consistency matters: prices, account settings, inventory. Both cache and DB are updated synchronously — slower writes but no data loss and no stale reads.
Q:How do you handle stale cache data?
A: Multiple approaches: (1) TTL — set an expiration time. Data is automatically refreshed after TTL. Simple but data is stale until expiry. (2) Invalidation on write — when data changes, delete the cache key. Next read fetches fresh data. More consistent but requires coordination. (3) Write-through — writes update cache and DB simultaneously. No stale data but higher write latency. (4) Stale-while-revalidate — serve stale data immediately, refresh in background. Best UX but briefly stale. Choose based on how stale is acceptable.
Your product page loads in 2 seconds due to 8 database queries
How would you add caching to improve performance?
Answer: Use cache-aside for each query result. Product details (TTL 10min), reviews (TTL 30min), related products (TTL 1hr). With a 90% cache hit rate, 7 of 8 queries are served from Redis in ~1ms each. The page loads in ~50ms instead of 2 seconds. For the price, use write-through to ensure it's always accurate. Invalidate cache keys when products are updated. Monitor cache hit rate — if it drops below 80%, investigate TTL settings or cache size.
Your analytics service processes 100K events per second
How would you handle the write load?
Answer: Write-behind. Each event is written to Redis (~1ms). A background worker flushes batches to the database every 5 seconds (or every 10K events). This reduces DB writes from 100K/sec to ~20 batch inserts/sec. Trade-off: if Redis crashes, up to 5 seconds of events are lost. For analytics, this is acceptable — the data is statistical, not transactional. If loss is unacceptable, use Kafka as a durable write-ahead buffer instead of Redis.
Common Mistakes
Not handling cache invalidation
Setting a 1-hour TTL and never invalidating on writes. A user updates their profile name, but the old name shows for up to an hour. Or worse: a product price changes but the cached old price is displayed — customers are charged the wrong amount.
✅Invalidate (delete) the cache key whenever the underlying data changes. Use cache-aside with invalidation-on-write as the baseline. TTL is a safety net for keys that slip through, not the primary freshness mechanism.
Over-caching everything
Caching every database query, including rarely accessed data. The cache fills up with cold data, evicting hot data. Cache hit rate drops. Memory is wasted. The cache becomes a slower, more expensive copy of the database.
✅Cache only hot data — the 20% that serves 80% of reads. Monitor cache hit rates per key pattern. If a key pattern has a <50% hit rate, it's not worth caching. Let cold data live in the database.
Ignoring stale data implications
Using cache-aside with a 30-minute TTL for inventory counts. During a flash sale, the cached count shows '50 in stock' while the real count is 0. Customers add items to cart, proceed to checkout, and get an error. Terrible user experience.
✅Match TTL to the data's staleness tolerance. Inventory: 5-10 seconds or invalidate on every purchase. Product description: 10-30 minutes. Static content: hours. Critical data (price, stock): write-through or very short TTL.
Choosing the wrong strategy for the use case
Using write-behind for financial transactions (risk of data loss) or write-through for analytics counters (unnecessary write latency). Each strategy has a specific sweet spot — using the wrong one either risks data or wastes performance.
✅Write-through for consistency-critical data (prices, balances). Write-behind for high-throughput, loss-tolerant data (counters, logs). Cache-aside for everything else. The strategy should match the data's importance and access pattern.