⚡ Guide: Caching — A Systems Design Deep Dive


⚡ Quick Dive

Overview & Key Takeaways

Caching is a critical system design pattern used to improve performance, reduce latency, and scale applications by avoiding repeated computation or I/O.

This guide covers:

  • What caching is and why it matters
  • Caching placement strategies (client, CDN, edge, app, DB)
  • Cache invalidation and consistency
  • Eviction policies
  • Common caching architectures and tools

🧠 1. What Is Caching?

Caching stores precomputed or previously retrieved data in a faster-access stora


📖 Extended Guide

Caching is a critical system design pattern used to improve performance, reduce latency, and scale applications by avoiding repeated computation or I/O.

This guide covers:

  • What caching is and why it matters
  • Caching placement strategies (client, CDN, edge, app, DB)
  • Cache invalidation and consistency
  • Eviction policies
  • Common caching architectures and tools

🧠 1. What Is Caching?

Caching stores precomputed or previously retrieved data in a faster-access storage layer, allowing future requests to bypass more expensive operations.

🚀 Why Cache?

  • Reduce database or API load
  • Lower latency for frequently accessed data
  • Smooth traffic spikes
  • Enable offline access (client-side)
// Without cache
user = db.query("SELECT * FROM users WHERE id = 42")

// With cache
user = cache.get("user:42")
if (!user) {
  user = db.query(...)
  cache.set("user:42", user)
}

📦 2. Caching Layers & Placement

Layer Description Example
Client-side In browser/app LocalStorage, IndexedDB
CDN Caches static assets near users Cloudflare, Akamai
Edge caching Dynamic content at PoPs Fastly, CloudFront Lambda@Edge
App-level Cache inside the app In-memory (e.g. Map, Guava)
Server-side Central cache layer Redis, Memcached
DB/Storage Internal query caching PostgreSQL buffer cache

🔁 3. Caching Strategies

🟢 Read-through Cache

App asks cache first; if not found, fetches from source and stores it.

function getUser(id) {
  let user = cache.get(`user:${id}`);
  if (!user) {
    user = db.query(...);
    cache.set(`user:${id}`, user);
  }
  return user;
}

🟠 Write-through Cache

Write to cache and the DB at the same time.

🔴 Cache-aside (Lazy loading)

App controls cache population manually.

🟣 Write-back (Write-behind)

Write only to cache and flush to DB asynchronously (dangerous on crashes).


⛔ 4. Cache Invalidation

“There are only two hard things in computer science: cache invalidation and naming things.”
— Phil Karlton

💣 Why It’s Hard:

  • When data changes, how do we know which cache keys to evict?
  • Cached data might become stale or inconsistent

🔧 Techniques:

  • Time-to-live (TTL): Expire keys automatically
  • Explicit Invalidation: App deletes/updates cache when DB changes
  • Versioning: Store with key version or hash (user:42:v3)
  • Event-driven: Use pub/sub (e.g. Redis Streams, Kafka) to evict across systems

🧹 5. Eviction Policies

Policy Description Use Case
LRU (Least Recently Used) Remove least recently accessed Most popular
LFU (Least Frequently Used) Remove least accessed frequently Hot/cold data separation
FIFO Remove oldest added Simple but naive
TTL-based Remove after N seconds Time-bound data like sessions
Manual Application deletes keys explicitly Fine-grained control
// Redis: set TTL
SET user:42 "data" EX 60

⚠️ 6. Consistency Models

Consistency Model Description Trade-offs
Strong Cache and source always in sync Hard to scale
Eventual Cache updated after source writes Simpler, may serve stale reads
Write-through Write to cache + DB together Safer, slightly slower
Write-back Write to cache only, sync later Fast, but risk of data loss

📚 7. Common Use Cases

Use Case Caching Strategy Tools
API responses TTL or CDN-based caching Fastly, Varnish
User profiles Cache-aside or read-through Redis
Search suggestions In-memory with LRU Guava, Caffeine (Java)
Product catalog Versioned cache + TTL Redis + async updates
ML features or scores Write-through, time-bounded Redis, feature stores

🛠️ 8. Tools and Technologies

Tool Type Notes
Redis In-memory, LRU, TTL, pub/sub Versatile, widely used
Memcached In-memory, LRU only Lightweight, simpler than Redis
Caffeine (Java) Local, LRU, async loading High-performance in-JVM caching
Varnish HTTP reverse proxy Edge and CDN caching
Cloudflare / Fastly CDN Global static and dynamic caching

🔥 9. Pitfalls to Avoid

Pitfall Recommendation
Serving stale data Use TTLs, versioned keys, or event triggers
Inconsistent multi-node caches Use central Redis or distributed cache
Cache stampede (thundering herd) Use locking or request coalescing
Large unbounded keys Use size limits and LRU eviction
Over-caching Don’t cache low-traffic or fast queries
// Prevent cache stampede
if (!cache.get(key)) {
  if (acquireLock(key)) {
    let val = db.query(...)
    cache.set(key, val);
    releaseLock(key);
  } else {
    waitAndRetry(); // someone else is loading
  }
}

🧰 10. Advanced Patterns

🧩 Sharded Caches

  • Split cache across multiple nodes
  • Redis Cluster, consistent hashing

🔁 Cache Invalidation by Events

  • Invalidate based on pub/sub changes
  • Kafka, Redis Streams, Debezium

📊 Cache Observability

  • Monitor hit/miss rates, TTL behavior, evictions
  • Use Prometheus exporters, Redis Insights, Datadog

✅ Summary

Topic Key Point
Strategy Cache-aside is most common
Placement Choose based on latency + scale
Invalidation TTL + versioning often best combo
Eviction LRU is best default
Consistency Choose based on criticality & latency

📚 Further Reading