system-design · intermediate

Caching Strategies — Aside, Through, Behind, and Refresh-Ahead

The Central Question

Consider a high-traffic financial and user management platform running on the CacheLab platform (cachelab.com) processing 50,000 requests per second:


Saying "we will use Redis" is an incomplete architectural answer.

A Caching Strategy defines the exact interaction protocol, ordering, and responsibility ownership between the application code, the cache layer (RAM), and the primary database (disk) during read and write operations.

This lesson answers one central question: How do Cache-Aside, Read-Through, Write-Through, Write-Behind (Write-Back), and Refresh-Ahead strategies manage read/write paths, and how do engineers choose the right pattern to balance read latency, write performance, and cache coherence?


The Taxonomy of Caching Strategies

Caching strategies fall into two operational axes: Read Patterns and Write Patterns.

flowchart TD
  Taxonomy[Caching Strategy Taxonomy] --> ReadPaths[Read Paths]
  Taxonomy --> WritePaths[Write Paths]
  
  ReadPaths --> CacheAside[1. Cache-Aside / Lazy Loading]
  ReadPaths --> ReadThrough[2. Read-Through]
  ReadPaths --> RefreshAhead[3. Refresh-Ahead]

WritePaths --> WriteThrough[4. Write-Through]
WritePaths --> WriteBehind[5. Write-Behind / Write-Back]
WritePaths --> WriteAround[6. Write-Around]

Figure 1: Taxonomy of read and write caching architectural patterns.


Read Strategies: Cache-Aside vs. Read-Through

1. Cache-Aside (Lazy Loading)

In **Cache-Aside**, the application code directly orchestrates reading from the cache, fetching from the database on miss, and populating the cache.
sequenceDiagram
    autonumber
    actor App as Application Code
    participant Cache as Redis RAM
    participant DB as Primary DB
    
    App->>Cache: 1. GET key
    alt Cache Hit (95%)
        Cache-->>App: Return Cached Value
    else Cache Miss (5%)
        Cache-->>App: Return Nil
        App->>DB: 2. SELECT * FROM table WHERE id = key
        DB-->>App: Return Row Payload
        App->>Cache: 3. SETEX key 3600 Payload
    end

Figure 2: Sequence diagram detailing Cache-Aside lazy loading execution.

2. Read-Through Caching

In **Read-Through**, the application treats the cache layer as a transparent single data store. The application requests data strictly from the cache service. On a miss, the **cache infrastructure itself** transparently loads data from the database.
sequenceDiagram
    autonumber
    actor App as Application Code
    participant CacheProxy as Cache Store (Read-Through)
    participant DB as Primary DB
    
    App->>CacheProxy: 1. Read Data (Key)
    alt Cache Hit
        CacheProxy-->>App: Return Cached Value
    else Cache Miss
        CacheProxy->>DB: 2. Internal DB Fetch
        DB-->>CacheProxy: Return Row
        CacheProxy-->>App: Return Value
    end

Figure 3: Read-Through proxy encapsulation model.


Write Strategies: Write-Through vs. Write-Behind vs. Write-Around

Handling database mutations while keeping the cache synchronized requires selecting a write strategy:

flowchart TD
  WriteStrategies[Write Caching Strategies] --> WT[1. Write-Through]
  WriteStrategies --> WB[2. Write-Behind / Write-Back]
  WriteStrategies --> WA[3. Write-Around]
  
  WT --> WTDesc["Synchronous Write to Cache AND DB simultaneously.<br/>High data consistency; higher write latency."]
  WB --> WBDesc["Async Write to Cache fast (sub-ms); batch write to DB later.<br/>Ultra-low write latency; risk of data loss on cache crash!"]
  WA --> WADesc["Write directly to DB, bypassing cache entirely.<br/>Prevents cache pollution for non-read data."]

Figure 4: Comparison of Write-Through, Write-Behind, and Write-Around patterns.

1. Write-Through Caching

2. Write-Behind / Write-Back Caching

3. Write-Around Caching

Refresh-Ahead (Predictive Caching)

In **Refresh-Ahead** caching, the cache layer automatically reloads cached items from the database *before* their TTL expires if the item is frequently accessed. By monitoring key read access velocity, the cache background process calculates whether a key is "hot". If a key with a 60-second TTL receives $>100\text{ RPS}$ and reaches 50 seconds of age, the cache automatically issues an asynchronous background database query to refresh the cached payload. When the key reaches 60 seconds, the payload is already fresh in memory, eliminating cache misses entirely for active items.

Cache Thundering Herd Prevention (Single-Flight Pattern)

When a hot key expires in a Cache-Aside architecture, hundreds of concurrent application threads experience a cache miss at the exact same millisecond. To prevent 100 concurrent database queries for the same key, systems implement the **Single-Flight Pattern** (e.g. Go `golang.org/x/sync/singleflight`). Single-flight suppresses duplicate in-flight execution by ensuring only 1 thread executes the database query while all other concurrent caller threads block and wait for that single thread's return result, sharing the same response payload across all callers.

Write-Through Rollback Semantics

In Write-Through caching, if the primary database write fails (due to SQL constraint violations or database connection timeouts), the cache layer must immediately rollback its local memory update and propagate the error exception back to the client. This guarantees that failed database mutations never leave stale draft data inside the cache layer.

Comparative Matrix: Caching Strategies

Strategy PatternPrimary Read LatencyPrimary Write LatencyData Consistency GuaranteeBest Real-World Use Case
Cache-AsideSub-ms (on hit)Normal DB speedEventual Consistency (TTL)General-purpose web APIs & profile reads.
Read-ThroughSub-ms (on hit)Normal DB speedHigh ConsistencyCentralized data access abstraction layers.
Write-ThroughSub-msHigh ($T_{\text{cache}} + T_{\text{DB}}$)Strong ConsistencyFinancial ledgers & user setting updates.
Write-Behind (Write-Back)Sub-msUltra-Low (sub-ms)Risk of Data LossHigh-volume IoT telemetry, gaming leaderboards.
Write-AroundSlow on first readNormal DB speedHigh ConsistencyLog streaming, historical archive writes.

Dual-Write Race Conditions & Invalidation Best Practices

In Cache-Aside architectures, updating data via simultaneous dual-writes triggers severe race conditions:

sequenceDiagram
    autonumber
    actor Writer1 as Thread 1 (Update Price to $10)
    actor Writer2 as Thread 2 (Update Price to $20)
    participant Cache as Redis Cache
    participant DB as PostgreSQL DB
    
    Writer1->>DB: 1. UPDATE products SET price = 10
    Writer2->>DB: 2. UPDATE products SET price = 20
    Writer2->>Cache: 3. SET product:10492 price = 20
    Writer1->>Cache: 4. SET product:10492 price = 10 (LATE ARRIVAL!)
    
    Note over Cache: Database price is $20, but Cache price is STALE $10 permanently!

Figure 5: Race condition caused by out-of-order dual-writes to cache.

Golden Rule of Cache Maintenance: Invalidate, Do Not Update!

Instead of updating the cached value on database mutations (`SET key new_val`), **always delete the key from the cache (`DEL key`)**:
  1. Thread 1 updates DB to $\$10$.
  2. Thread 1 deletes key from Cache (DEL key).
  3. Subsequent read triggers a clean Cache Miss, reading the authoritative $\$20$ from DB and populating the cache safely.

Complete Worked Example: Go Read-Through & Write-Behind Cache Manager

Let's inspect a complete Go implementation of a Read-Through and Write-Behind Cache Manager for the CacheLab platform (cachelab.com).

package main

import (
"context"
"fmt"
"sync"
"time"
)

type Database interface {
Read(key string) (string, error)
Write(key string, value string) error
}

type WriteBehindCache struct {
mu sync.RWMutex
cache map[string]string
writeBuffer chan struct{ k, v string }
db Database
}

func NewWriteBehindCache(db Database, bufferSize int) *WriteBehindCache {
c := &WriteBehindCache{
cache: make(map[string]string),
writeBuffer: make(chan struct{ k, v string }, bufferSize),
db: db,
}

go c.startAsyncFlusher()
return c
}

func (c *WriteBehindCache) ReadThrough(key string) (string, error) {
c.mu.RLock()
val, exists := c.cache[key]
c.mu.RUnlock()

if exists {
return val, nil // Cache Hit
}

// Cache Miss: Transparent DB Read
val, err := c.db.Read(key)
if err != nil {
return "", err
}

c.mu.Lock()
c.cache[key] = val
c.mu.Unlock()

return val, nil
}

func (c *WriteBehindCache) WriteBehind(key string, value string) {
c.mu.Lock()
c.cache[key] = value // Sub-ms RAM write
c.mu.Unlock()

// Enqueue for async background batch flush to DB
c.writeBuffer <- struct{ k, v string }{k: key, v: value}
}

func (c c WriteBehindCache) startAsyncFlusher() {
for item := range c.writeBuffer {
err := c.db.Write(item.k, item.v)
if err != nil {
fmt.Printf("[FLUSH ERROR] Failed to write key %s to DB: %v\n", item.k, err)
}
}
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. Write-Behind Data LossCache node experiences hard hardware failure while un-flushed writes remain in memory queue.Data written to API disappears permanently before hitting primary database.Un-flushed buffer depth metric $> 0$ at crash time.Use Write-Ahead Logging (WAL) or deploy dual-replicated Redis instances.
2. Dual-Write Cache Stale RaceThreads execute SET key val out-of-order following database mutations.Cache holds outdated value while database contains newer value permanently.User bug reports regarding stale account prices or profile attributes.Always Delete Cache Keys (DEL key) on database mutations instead of setting values.
3. Cache PollutionWriting bulk batch export data to cache via Write-Through strategy.Hot application data evicted from RAM by one-off bulk export keys.Sudden dip in overall Cache Hit Ratio ($H$) following batch jobs.Use Write-Around Strategy for bulk data operations to bypass cache entirely.
4. Write-Through Latency SpikeSynchronous DB write blocks client responses during database disk I/O surges.API write latency surges from $2\text{ms}$ to $450\text{ms}$ during DB load.High $P_{99}$ HTTP POST endpoint latency alerts.Switch write-heavy paths to Write-Behind or asynchronous queues.

What You Should Remember

  1. Match strategy to workload read/write profile: Use Cache-Aside for general reads, Write-Through for strong financial consistency, and Write-Behind for high-throughput ingestion.
  2. Invalidate keys on DB mutation: Delete keys (DEL key) on writes instead of updating them (SET key) to prevent out-of-order stale race conditions.
  3. Write-Behind provides max speed at data loss risk: Write-Behind offers sub-millisecond writes, but risks losing un-flushed buffer queues if cache nodes crash.
  4. Use Write-Around to prevent cache pollution: Bypass the cache for bulk historical data writes to keep RAM focused on hot operational keys.
  5. Read-Through encapsulates database access: Move cache miss DB fetching into the cache proxy to simplify application code across microservices.

Glossary of Terms

TermDefinition
Cache-AsideA lazy-loading pattern where application code reads from cache first and handles database fallback.
Read-ThroughA caching pattern where the cache proxy transparently fetches data from the DB on cache misses.
Write-ThroughA synchronous write pattern where updates are committed to both cache and DB before returning success.
Write-Behind (Write-Back)An asynchronous write pattern where updates hit RAM fast and flush to DB in background batches.
Write-AroundA write pattern that bypasses the cache entirely, writing directly to the primary database.
Cache InvalidationThe process of deleting or expiring cached keys when underlying database records are updated.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing the storage tier for an IoT fleet management platform (`iot.cachelab.com`): **Questions**:
  1. Select the optimal read and write caching strategies for this IoT fleet workload.
  2. Formulate the data loss mitigation architecture if choosing Write-Behind caching.

Interactive Self-Assessment

Deleting keys avoids out-of-order race conditions, forcing subsequent reads to fetch the authoritative database value.

Deleting keys automatically formats the underlying NVMe SSD disk drives.

Deleting keys revokes client HTTPS TLS encryption certificates on edge routers.

Deleting keys doubles the physical hardware clock speed of primary database CPUs.

A cache node crash before the asynchronous queue flushes to disk causes permanent loss of un-written transactions.

Write-Behind converts SQL primary keys into random UUID strings.

Write-Behind automatically updates public ISP DNS nameservers.

Write-Behind downgrades the Linux operating system kernel version.


What to Learn Next

Track: Data, Storage and Messaging

Previous: Caching 101 — Memory Offloading and Latency Reduction

Next: Content Delivery Networks (CDN) — Edge Acceleration and Caching

Series: Caching

  1. Caching 101 — Memory Offloading and Latency Reduction
  2. Caching Strategies — Aside, Through, Behind, and Refresh-Ahead (this guide)
  3. Cache Eviction Policies — LRU, LFU, TTL, and Friends
  4. Distributed Caching — Sharding and High-Availability Clusters
  5. Cache Stampede — When Expiry Melts the Database
  6. Stale Cache After Write — When Your Own Update Disappears
  7. Content Delivery Networks (CDN) — Edge Acceleration and Caching
  8. Bloom Filters — Probabilistic Set Membership at Scale

All series

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab