Stage 4 · Provision
Database Scaling Strategies
Caching Layers
Redis, Memcached, local caches — cache-aside, read-through, and write-through strategies.
Why Caching?
Caching stores frequently accessed data in fast storage (memory) to avoid expensive database queries or computations. A single Redis instance can serve 100K+ QPS with sub-millisecond latency, compared to 1-10ms for a database query. Caching is the highest-ROI performance optimization.
Cache Strategies
| Strategy | Read Path | Write Path | Use Case |
|---|---|---|---|
| Cache-Aside | App checks cache, then DB | App invalidates cache | Most common, flexible |
| Read-Through | Cache fetches from DB on miss | App invalidates cache | Simpler app code |
| Write-Through | App reads from cache | App writes to cache + DB | Strong consistency |
| Write-Behind | App reads from cache | App writes to cache, async to DB | Write-heavy workloads |
import redis
r = redis.Redis()
def get_user(user_id: int) -> dict:
# 1. Check cache
cached = r.get(f"user:{user_id}")
if cached:
return json.loads(cached)
# 2. Cache miss — fetch from DB
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 3. Populate cache with TTL
r.setex(f"user:{user_id}", 300, json.dumps(user))
return userCache-aside is the most common pattern. The application controls both reads and writes. On cache miss, the app fetches from DB and populates the cache. On write, the app invalidates the cache.
Redis vs Memcached
| Feature | Redis | Memcached |
|---|---|---|
| Data structures | Strings, lists, sets, hashes, sorted sets | Strings only |
| Persistence | RDB + AOF | None |
| Clustering | Redis Cluster | Client-side sharding |
| Memory efficiency | Higher overhead | Lower overhead |
| Pub/Sub | Yes | No |
| Lua scripting | Yes | No |
Local In-Process Caches
Local caches (Caffeine, Guava Cache) run in the same process as the application. They provide nanosecond access but are limited to a single instance. Use them for hot data that changes infrequently — configuration, feature flags, reference data.
Cache<String, User> userCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.refreshAfterWrite(Duration.ofMinutes(1)) // Async refresh
.build(key -> loadUserFromDB(key));Caffeine provides automatic loading, eviction, and refresh. The refreshAfterWrite option asynchronously refreshes hot entries before they expire, avoiding cache stampedes.
Multi-Layer Cache Topology
Request
│
▼
L1: Local Cache (Caffeine) ── 1ns, 10K entries
│ miss
▼
L2: Distributed Cache (Redis) ── 0.5ms, 1M entries
│ miss
▼
L3: Database (PostgreSQL) ── 5ms, unlimitedEach layer is smaller and faster. L1 handles the hottest data, L2 handles warm data, L3 is the source of truth. Check each layer sequentially.
Cache Consistency
Cache consistency is the challenge of keeping cached data in sync with the database. When the database updates, the cache must be invalidated or updated. Event-driven invalidation using CDC (Change Data Capture) is the most reliable approach for distributed caches.
Always set a TTL on cached data, even with active invalidation. If invalidation fails (bug, network partition), the TTL ensures stale data eventually expires. TTL is the last line of defense against stale reads.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.