Cache-AsideRead-ThroughWrite-ThroughWrite-BehindRedisCachingPerformance

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.

24 min read9 sections
01

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.

02

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
The Impact of Caching — By the Numberstext
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.

03

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

1

Check the cache

Application asks the cache: 'Do you have user #42?' If yes (cache hit) → return immediately. Done.

2

On miss → fetch from database

Cache doesn't have it. Application queries the database: SELECT * FROM users WHERE id = 42. Gets the result.

3

Store in cache

Application writes the result to the cache with a TTL (e.g., 5 minutes): SET user:42 → {name: 'Alice', ...} EX 300.

4

Return response

Return the data to the client. Next request for user #42 will be a cache hit.

Cache-Aside — Pseudocodetext
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.

04

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.

Read-Through vs Cache-Asidetext
Cache-Aside (application manages):
  Appcheck cachemissApp queries DBApp writes to cache

Read-Through (cache manages):
  Appcheck cachemissCache queries DBCache 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.

1

Application writes to cache

Application sends the write to the cache layer: 'Set user #42 name to Alicia.'

2

Cache writes to database

The cache synchronously writes the same data to the database. Both stores now have the same value.

3

Acknowledge to application

Only after BOTH cache and database confirm, the write is acknowledged. The application knows the data is consistent everywhere.

Write-Through Flowtext
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.

05

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.

1

Application writes to cache

'Set user #42 name to Alicia.' The cache accepts the write and stores it in memory.

2

Acknowledge immediately

The cache responds: 'Write confirmed.' The application continues. The database hasn't been touched yet.

3

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.

Write-Behind Flowtext
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 cacheconfirmed
  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.

06

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.

1

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).

2

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).

3

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.

4

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.

5

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.

Architecture Summarytext
Product Page Request Flow:

  ClientApp ServerRedis 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.

07

Trade-offs & Decision Making

StrategyWrite LatencyRead ConsistencyData Loss RiskComplexityBest For
Cache-AsideN/A (writes go to DB)Stale until TTL/invalidationNoneLowGeneral purpose, most use cases
Read-ThroughN/A (reads only)Stale until TTLNoneMediumTransparent caching layer
Write-ThroughHigh (cache + DB sync)Always freshNoneMediumConsistency-critical data
Write-BehindVery low (cache only)Fresh in cache, stale in DBYes (cache crash)HighWrite-heavy, loss-tolerant

Consistency vs Performance

DimensionHigh ConsistencyHigh Performance
StrategyWrite-throughWrite-behind
Write latency50-100ms (wait for DB)1ms (cache only)
Stale readsNeverPossible (DB lags behind cache)
Data loss riskNoneYes (unflushed writes)
Use whenPrice, balance, inventoryCounters, logs, analytics

Freshness vs Speed

TTLFreshnessCache Hit RateDB Load
No cacheAlways fresh0%Maximum
10 secondsVery fresh~70%Low
5 minutesModerately fresh~90%Very low
1 hourPossibly stale~98%Minimal
No expiryStale 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.

08

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.

1

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.

2

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.

09

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.