Skip to main content

Distributed Caching

Caching is often the difference between a system that handles 100 QPS and one that handles 1 million QPS. This module covers everything from cache fundamentals to advanced distributed caching patterns. The fundamental insight behind caching is straightforward: if you ask the same question repeatedly, remembering the answer is faster than re-computing it. It is the same reason you keep your phone number in contacts instead of looking it up in the phone book every time. But distributed caching introduces a twist that makes it one of the hardest problems in systems design: when the underlying answer changes, how do you make sure every cached copy is updated? This is the cache invalidation problem, and it is where most production cache bugs live.
Track Duration: 10-14 hours
Key Topics: Cache Strategies, Consistency, Invalidation, Redis, Memcached, Cache Stampede
Interview Focus: Cache consistency, invalidation strategies, hot key handling

Why Caching Matters


Cache Strategies

Read Strategies

Application manages cache explicitly. Most common pattern.
Pros: Simple, only caches what’s needed, cache failures don’t break reads Cons: Cache miss = slow first read, potential for stale data

Write Strategies

Write to cache AND database synchronously.
Pros: Cache always consistent, no stale data Cons: Higher write latency, both must succeed

Cache Invalidation

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

Invalidation Strategies

The Delete vs Update Debate

Why Delete is Usually Better (this is one of the most important cache design decisions you will make):
The rule of thumb a senior engineer follows: Invalidate (delete) on write, populate on read. This is simpler, safer against race conditions, and wastes less cache space on data that might never be read again. Only use “update on write” if you have measured that the cache-miss penalty after deletion is unacceptable AND you can guarantee ordering (e.g., via compare-and-swap).

Cache Stampede (Thundering Herd)

When a popular cache key expires, many requests simultaneously hit the database.

Stampede Prevention Strategies

Only one request fetches, others wait.
Pros: Prevents duplicate DB queries Cons: All waiters blocked on one request

Hot Key Problem

When one key gets disproportionate traffic.

Hot Key Solutions

Add in-process cache before Redis.
Pros: Extremely fast, reduces L2 load Cons: Inconsistency between instances, memory usage

Advanced: Sidecar Caching & The “Mesh” Pattern

At Staff/Principal level, you must move beyond the “application-manages-cache” (Cache-Aside) model. In a microservices architecture, managing cache logic in every service leads to duplication and inconsistency.

1. Sidecar Caching (e.g., Pelikan, Mcrouter)

Instead of the application connecting directly to Redis, it connects to a Local Sidecar Proxy.
  • Benefits:
    • Language Agnostic: The sidecar handles complex logic (consistent hashing, retries, failover).
    • Connection Pooling: Reduces the number of open connections to the central Redis cluster.
    • Request Coalescing: The sidecar can merge multiple identical requests from the local app into one upstream request.

2. EVCache (The Netflix Pattern)

EVCache is a distributed, sharded, replicated in-memory caching system based on Memcached.
  • Sidecar-based: Every EC2 instance has an EVCache sidecar.
  • Global Replication: Writes are replicated asynchronously to other AWS regions, allowing for Local Reads globally.

3. Layered Cache Hierarchy (EVCache vs. Redis)

Staff Tip: Sidecar caching is the only way to scale caching in a Polyglot Microservices environment. It decouples the “How to cache” (retries, hashing) from the “What to cache” (business logic).

Distributed Cache Architectures

Redis Cluster

Memcached vs Redis


Cache Consistency Patterns

Eventual Consistency

Double-Delete Implementation


Advanced Design Scenarios

Scenario 1: Multi-Level Caching for Global APIs

Your company exposes a global read-heavy API (product catalog, content, or configuration) with users across multiple regions. You want sub-100ms p99 latencies while still keeping a single source-of-truth database.
Core ideas:
  • Cache as many layers as possible:
    • Edge (CDN): cache public, cacheable responses with long TTLs and ETag/Last-Modified for validation.
    • Regional gateway (L1.5): shared Redis or Envoy cache per region for JSON APIs.
    • App instances (L2): small in-process L1 caches for the hottest keys.
  • Key design:
    • Include versioning in keys for global invalidation: product:{id}:v{schema_version}.
    • For per-tenant data: tenant:{tenant_id}:product:{id}:v{version}.
  • Invalidation:
    • For rarely-changing data, use long TTLs + explicit purge when changes happen.
    • On update, clear from inner layers first (DB → regional Redis → CDN purge API).
Failure modes & strategies:
  • CDN down or misbehaving: have monitoring that can bypass CDN and hit regional gateway directly.
  • Regional cache cluster degraded: instances fall back to L1 in-process cache + direct DB reads with tighter rate limits.
  • Schema changes: bump schema_version in cache keys to avoid mixed old/new value formats.

Scenario 2: Configuration and Feature Flag Caching

You have a central configuration/feature flag service that many microservices depend on. Latency spikes or downtime in this service must not take down the entire fleet. Requirements:
  • Services should start even if the config service is temporarily unavailable.
  • Each service instance should cache configuration locally and refresh in the background.
  • Changes should propagate within seconds, not minutes.
Architecture:
  • Source of truth: configuration stored in a durable DB (e.g., Postgres) behind a config service.
  • Distributed cache: Redis/Etcd used by the config service to cache hot config blobs (config:service_name).
  • Service-local cache: each microservice maintains an in-process cache of parsed config.
  • Notification channel: a message bus (Kafka, SNS/SQS, Redis Pub/Sub) carries “config changed” events.
Design notes:
  • On startup, services block briefly to fetch essential config; thereafter they serve from local memory.
  • If the config service and cache are both down, services can keep using the last known good config in memory.
  • For safety-sensitive flags, include expiry timestamps so services can disable risky behavior if config has not refreshed in too long.

Scenario 3: Bounded-Staleness Caching on Top of a Strong Database

Sometimes you want strictly serializable writes but are happy to serve data up to (T) seconds old for reads. This is common for dashboards, search results, and secondary views. Goal: “Reads may be stale by at most T seconds, never more. Writes must always see their own effect.” Design:
  • Keep strong consistency in the primary DB (e.g., Spanner/CockroachDB/Postgres with serializable isolation).
  • Layer a cache in front with staleness metadata per key.
Write path:
  • On write, delete cache (or write a fresh CachedValue with current last_write_time).
  • If you have a write-heavy workload, combine this with double-delete or CDC-backed invalidation to avoid stale repopulation races.
Trade-offs:
  • You can tighten or loosen MAX_STALENESS_SEC per endpoint: 1–2s for user-facing dashboards, 30–60s for admin analytics.
  • Reads that cannot tolerate any staleness bypass cache entirely and always hit the DB.

Interview Questions

Key considerations:
  1. Cache structure:
    • User’s feed: feed:{user_id} → List of post IDs
    • Individual posts: post:{post_id} → Post data
    • Two-level: Feed IDs (small) + Post content (larger, reusable)
  2. Invalidation strategy:
    • New post: Fan-out to update all follower feeds
    • For large fan-out (celebrities): Don’t fan-out, pull on read
    • Post edit/delete: Delete post cache, let feed IDs remain
  3. Hot keys (celebrity posts):
    • Local L1 cache + remote L2
    • Replicate across shards
    • Consider CDN for very popular content
  4. Consistency:
    • Eventual is usually acceptable for feeds
    • Author should see own posts immediately (read-your-writes)
Cache-Aside (lazy loading):
  • Read-heavy workloads
  • When not all data needs to be cached
  • When cache failure shouldn’t break writes
  • Most common for web applications
Write-Through:
  • When cache consistency is critical
  • Write-heavy workloads where you need fast subsequent reads
  • When you can tolerate higher write latency
Write-Behind:
  • Write-heavy, read-light workloads
  • When you can batch database writes
  • When you can tolerate potential data loss
  • Gaming leaderboards, view counts, etc.

Key Takeaways

Cache Strategically

Not everything needs caching. Cache hot paths where latency matters most.

Plan for Invalidation

Design invalidation strategy upfront. Delete is usually safer than update.

Handle the Stampede

Popular keys + expiration = danger. Use locking, probabilities, or stale serving.

Accept Eventual Consistency

For most use cases, eventual is fine. Build UX around it.