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:
- Service A (User Account Profile): Receives 50,000 read queries/sec and 5 write queries/sec. Data updates must be visible within 1 second.
- Service B (IoT Telemetry Aggregator): Ingests 100,000 write events/sec into persistent storage. Writing directly to a disk-bound database causes continuous I/O saturation.
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.
- Responsibility: Application code handles cache misses and updates.
- Pros: Cache stores only data that is actively requested; node crashes do not disrupt primary database reads.
- Cons: Initial read for any new key suffers a Cache Miss penalty (3-way round trip).
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
- Flow: Application writes data to the cache layer. The cache layer synchronously writes the update to the primary database before confirming
HTTP 200to the client. - Trade-off: Guarantees cache consistency and zero stale reads, but increases write latency ($T_{\text{write}} = T_{\text{cache}} + T_{\text{DB}}$).
2. Write-Behind / Write-Back Caching
- Flow: Application writes data to the cache layer in sub-milliseconds ($1.5\text{ms}$). An asynchronous background worker batches updates and writes them to the database later.
- Trade-off: Provides maximum write throughput ($100,000\text{ writes/sec}$), but introduces Data Loss Risk if the cache node crashes before flushing un-written buffers to disk.
3. Write-Around Caching
- Flow: Application writes data directly to the primary database, bypassing the cache entirely.
- Trade-off: Avoids populating the cache with keys that may never be read again (preventing cache pollution), but causes a guaranteed Cache Miss on the first subsequent read.
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 Pattern | Primary Read Latency | Primary Write Latency | Data Consistency Guarantee | Best Real-World Use Case |
|---|---|---|---|---|
| Cache-Aside | Sub-ms (on hit) | Normal DB speed | Eventual Consistency (TTL) | General-purpose web APIs & profile reads. |
| Read-Through | Sub-ms (on hit) | Normal DB speed | High Consistency | Centralized data access abstraction layers. |
| Write-Through | Sub-ms | High ($T_{\text{cache}} + T_{\text{DB}}$) | Strong Consistency | Financial ledgers & user setting updates. |
| Write-Behind (Write-Back) | Sub-ms | Ultra-Low (sub-ms) | Risk of Data Loss | High-volume IoT telemetry, gaming leaderboards. |
| Write-Around | Slow on first read | Normal DB speed | High Consistency | Log 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`)**:- Thread 1 updates DB to $\$10$.
- Thread 1 deletes
keyfrom Cache (DEL key). - 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 Scenario | Root Cause | System Symptom | Detection Signal | Mitigation Strategy |
|---|---|---|---|---|
| 1. Write-Behind Data Loss | Cache 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 Race | Threads 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 Pollution | Writing 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 Spike | Synchronous 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
- 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.
- 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. - 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.
- Use Write-Around to prevent cache pollution: Bypass the cache for bulk historical data writes to keep RAM focused on hot operational keys.
- Read-Through encapsulates database access: Move cache miss DB fetching into the cache proxy to simplify application code across microservices.
Glossary of Terms
| Term | Definition |
|---|---|
| Cache-Aside | A lazy-loading pattern where application code reads from cache first and handles database fallback. |
| Read-Through | A caching pattern where the cache proxy transparently fetches data from the DB on cache misses. |
| Write-Through | A 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-Around | A write pattern that bypasses the cache entirely, writing directly to the primary database. |
| Cache Invalidation | The 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`):- 500,000 trucks transmit GPS location coordinates every 5 seconds ($100,000\text{ write RPS}$).
- Truck coordinates are read by dispatchers only when investigating incidents ($100\text{ read RPS}$).
- Select the optimal read and write caching strategies for this IoT fleet workload.
- 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
- Cache Eviction Policies — LRU, LFU, FIFO, ARC: Master memory eviction algorithms when RAM hits capacity.
- Distributed Caching & Redis Cluster: Learn consistent hashing and Redis cluster partitioning.
- Caching 101 — Memory Offloading and Latency Reduction: Revisit RAM vs disk latency physics and hit ratios.
Track: Data, Storage and Messaging
Previous: Caching 101 — Memory Offloading and Latency Reduction
Next: Content Delivery Networks (CDN) — Edge Acceleration and Caching
Series: Caching
- Caching 101 — Memory Offloading and Latency Reduction
- Caching Strategies — Aside, Through, Behind, and Refresh-Ahead (this guide)
- Cache Eviction Policies — LRU, LFU, TTL, and Friends
- Distributed Caching — Sharding and High-Availability Clusters
- Cache Stampede — When Expiry Melts the Database
- Stale Cache After Write — When Your Own Update Disappears
- Content Delivery Networks (CDN) — Edge Acceleration and Caching
- Bloom Filters — Probabilistic Set Membership at Scale
By Shubham Jain