Caching Strategy Guide
The decision
Caching is a strategy for storing copies of data in fast-access storage to avoid repeatedly fetching it from slower sources (databases, external APIs, computation). The fundamental question is not whether to cache, but how: specifically, who is responsible for loading data into the cache, and when do writes update the cache versus the database. The answer determines consistency guarantees, failure modes, and implementation complexity.
The four primary caching strategies are cache-aside (lazy loading), where the application manages all cache interactions; write-through, where all writes go to both cache and database synchronously; write-behind (write-back), where writes update the cache immediately but database updates are deferred asynchronously; and read-through, where the cache tier intercepts reads and handles cache misses transparently. Each strategy represents a different trade-off between consistency, write performance, read performance, and operational complexity.
Caching is not just about Redis or Memcached. HTTP caching at CDN and browser layers, in-process application caches (JVM heap, local dictionaries), database query caches, and service mesh response caches all apply the same principles. The strategy decision applies at every caching layer, and the right answer differs by layer. A product page might use write-through caching for inventory counts (strong consistency needed) but CDN caching with cache-aside refresh for product descriptions (eventual consistency acceptable, high read volume).
Why it matters
Caching often provides the single largest performance improvement available to an application. Database queries that take 50ms can be served from Redis in under 1ms—a 50x latency reduction. CDN caching eliminates entire server round trips for static and semi-static content, reducing origin load by 80-95% for content-heavy applications. Correctly implemented caching transforms the scaling economics of an application, often allowing a much smaller fleet of application servers and databases to serve orders of magnitude more traffic.
The risk is that caching introduces a second source of truth. When the database and cache disagree, applications serve stale data—incorrect product prices, outdated inventory, wrong user permissions. Cache invalidation is famously called one of the two hard problems in computer science because it requires correctly predicting all the ways data can change and all the places it might be cached. A misconfigured cache TTL that serves a 24-hour-old price list for five minutes during a flash sale causes real financial and reputational damage.
Cache failures have asymmetric effects. When a cache works, it is invisible—fast, transparent, cheap. When a cache fails (Redis cluster unreachable, cold start after deployment), the application falls back to the database, and if the database is sized to handle only the cache-miss load, a cache stampede—thousands of simultaneous cache misses hitting the database—can take the entire system down. This thundering herd problem means that caching decisions must include failure mode analysis: what happens when the cache is empty or unavailable?
Choose Cache-Aside when
- The application needs fine-grained control over what is cached and for how long
- Read-heavy workload where cache misses are acceptable and data can tolerate some staleness
- The cache can be lost and rebuilt from the database without data loss (cache is disposable)
- Different data types need different caching strategies within the same application
- Application-level cache key construction is complex (e.g., user-specific, locale-specific keys)
Choose Write-Through when
- Data must be consistent between cache and database at all times—no stale reads acceptable
- Read performance is more critical than write performance (writes pay the cache-fill cost upfront)
- Subsequent reads of recently written data must be fast without incurring a cache miss
- The data access pattern shows high read-after-write locality (write a record, immediately read it)
Comparison
CACHE-ASIDE (Lazy Loading)
═══════════════════════════════════════════════════
READ: WRITE:
App → Cache (miss) App → Database ✓
App → Database App → Invalidate cache key
App → Cache (set, TTL) (or skip cache invalidation)
App → Return data
Pros: Simple, only caches read data, cache failure degrades gracefully
Cons: Cache miss on first read (latency spike), stale data until TTL expires
WRITE-THROUGH
═══════════════════════════════════════════════════
READ: WRITE:
App → Cache (hit, fresh) App → Cache (set)
App → Return data Cache → Database (sync)
Return to App when both done
Pros: Cache always has fresh data, no stale reads
Cons: Write latency = cache + DB write, cache filled with unread data
WRITE-BEHIND (Write-Back)
═══════════════════════════════════════════════════
READ: WRITE:
App → Cache (hit, fresh) App → Cache (set immediately)
App → Return data Return to App ← FAST
Background: Cache → Database (async)
Pros: Fastest write path, batching reduces DB write load
Cons: Data loss risk if cache fails before DB write, complex recovery
READ-THROUGH
═══════════════════════════════════════════════════
READ: WRITE:
App → Cache (miss) App → Database (or cache-aside)
Cache → Database (auto-fill) Cache invalidated or updated
Cache → Return to App
Pros: Application logic simplified (cache handles misses)
Cons: First-request latency, requires cache provider support (e.g. Memcached with plugins)
STRATEGY SELECTION GUIDE
════════════════════════════════════════════════════════════════
Read-heavy?
│
┌────────┴────────┐
YES NO (write-heavy)
│ │
Staleness OK? Write latency critical?
│ │
┌────────┴────────┐ ┌─────┴─────┐
YES NO YES NO
│ │ │ │
Cache-Aside Write- Write- Write-
(TTL-based) Through Behind Through
Trade-offs
Write-Through strengths
- Cache is always consistent with the database—no stale data concerns for recent writes
- Subsequent reads after writes are always cache hits, providing consistent read performance
- Simpler reasoning about data consistency than eventually-consistent alternatives
- No thundering herd on cache restart—cache is pre-populated on writes rather than populated on demand
Write-Through weaknesses
- Write latency includes both the cache write and database write—higher than cache-aside or write-behind
- Cache fills with data that may never be read if write patterns don't match read patterns
- Cache size must accommodate all written data, not just frequently-read data
- Write failures are complex: if the cache write succeeds but the database write fails (or vice versa), the two are inconsistent
Implementation considerations
For cache-aside, TTL selection is the most consequential configuration decision. Too short a TTL defeats the purpose of caching; too long causes stale data issues. A useful heuristic: set TTL to the maximum staleness the business can tolerate for that data type. User session data: 30 minutes. Product descriptions: 1 hour. Pricing and inventory: 60 seconds or use event-driven invalidation. Reference data (country codes, categories): 24 hours. Apply cache stampede protection by adding jitter to TTLs (randomize within ±10% of target TTL) so that mass cache expiration events don't trigger simultaneous database load spikes.
For write-behind caching, the asynchronous database write introduces a window where data exists only in cache. If the cache node fails during this window, the write is lost. Production write-behind implementations require the cache to persist queued writes to disk (Redis AOF mode) and handle write failure with retry logic and dead letter handling for failed writes. This complexity often means write-behind is reserved for high-frequency, low-criticality writes (view counts, click tracking, metrics aggregation) rather than transactional data.
Cache eviction policies determine what happens when the cache is full. Redis supports LRU (Least Recently Used), LFU (Least Frequently Used), TTL-based, and random eviction. For cache-aside with unpredictable access patterns, LFU maximizes hit rate. For session caches, LRU ensures recent users are retained. Allkeys-LRU evicts any key; volatile-LRU evicts only keys with TTLs set, which provides a safety net: keys without TTL (critical data) are never evicted. Always configure a maxmemory-policy explicitly rather than relying on defaults, which vary by Redis version.
Common mistakes
- No cache stampede protection: When many cache keys expire simultaneously (e.g., after a cache restart or scheduled mass expiry), thousands of simultaneous database queries cause a thundering herd; use probabilistic early expiration or mutex-locked cache fills.
- Caching mutable shared objects by reference: Storing a mutable object in a local in-process cache and modifying it results in the cached version also changing; always serialize (deep copy) data before caching.
- Forgetting to invalidate on writes: Cache-aside without invalidation on writes serves stale data indefinitely; every write path must either invalidate or update the cache entry for affected keys.
- No eviction policy on Redis: A Redis instance configured without a maxmemory-policy will use all available memory and then begin returning OOM errors, crashing the application; always set maxmemory and maxmemory-policy.
- Caching non-deterministic data: Caching current time, random numbers, or data that changes per request is ineffective at best and introduces subtle bugs at worst; only cache deterministic, stable data.
Decision checklist
- What is the read-to-write ratio? High read ratio favors caching; write-heavy workloads get less benefit.
- What staleness is acceptable for this data? Pricing and permissions require near-zero; product descriptions tolerate minutes.
- What happens if the cache is unavailable? Is your database sized to handle full traffic load as a fallback?
- Does the data change frequently enough that long TTLs serve stale data, or infrequently enough that TTLs are safe?
- Is the cache used as a system of record (write-behind) or purely as a read accelerator (cache-aside, write-through)?
- Are there cache key collision risks between different users, locales, or versions of the application?
- Have you modeled the cache eviction behavior under memory pressure?
Real-world examples
- Twitter's timeline caching: Twitter uses cache-aside with Redis to cache precomputed home timelines for users. When a user follows someone new or tweets are deleted, specific cache keys are invalidated rather than waiting for TTL expiry—event-driven invalidation for high-consistency requirements rather than TTL-based eventual consistency.
- Facebook's Memcached infrastructure: Facebook runs one of the world's largest Memcached deployments using cache-aside at scale, with thousands of Memcached servers per region. Their published research on cache invalidation and consistency ("Scaling Memcache at Facebook") is a seminal reference for large-scale cache-aside implementations including their regional invalidation protocol.
- E-commerce checkout flows: Most e-commerce platforms use write-through for shopping cart data: cart writes go to both cache and database synchronously, ensuring that users immediately read back their just-added items without cache misses while also ensuring cart state is never lost to a cache failure between writes.
Related decisions
- SQL vs NoSQL — Redis and Memcached as key-value stores complement relational databases
- Scalability — caching as a scalability pattern alongside database read replicas
- Reliability — cache failure modes and fallback strategies
- Data Architecture — caching in the context of broader data flow design