Cache-aside, write-through, write-behind, TTL, cache eviction — Redis in production.
Published September 21, 2026
A cache keeps a copy of data somewhere faster or closer than its source, so that repeated requests skip the expensive work. Reading from local memory takes nanoseconds, a Redis round trip about a millisecond, and a database query with disk access can take tens of milliseconds. Caching is usually the single largest lever for read latency and database load.
The famous difficulty is the other side: a cache is a copy, and copies go stale. Every caching design is a trade-off between speed and freshness.
browser cache ─▶ CDN edge ─▶ load balancer / reverse proxy ─▶ app (in-process cache) ─▶ distributed cache (Redis) ─▶ database (its own buffer cache)
Cache-Control, ETag): best for static assets and public, rarely changing content.The application checks the cache first. On a miss, it reads the database and populates the cache:
public Product getProduct(String id) {
String key = "product:" + id;
Product cached = redis.get(key);
if (cached != null) return cached; // hit
Product product = productRepository.findById(id).orElseThrow();
redis.set(key, product, Duration.ofMinutes(10)); // populate with a TTL
return product;
}
The cache itself loads missing entries from the source, through a configured loader. It's the same idea as cache-aside, but the logic lives in the cache layer (for example Caffeine's LoadingCache, or Spring's @Cacheable).
| Pattern | How it works | Pros | Cons |
|---|---|---|---|
| Write-around + invalidate | Write to the DB, then delete the cache key. The next read reloads it | Simple, and the usual partner of cache-aside | The next read is a miss |
| Write-through | Write to the cache and the DB synchronously, together | The cache is always warm and fresh | Slower writes, and caches data that may never be read |
| Write-behind (write-back) | Write to the cache. It's flushed to the DB asynchronously | Very fast writes, and batching | Data loss if the cache fails before flushing. Complex |
Delete, don't update, on write. If two concurrent writers each update the cache after writing the database, they can finish in the wrong order and leave a stale value cached indefinitely. Deleting the key forces the next read to fetch the current value. A narrow race still exists (a slow reader re-populating an old value right after the delete). TTLs bound its impact, and some teams do a second, delayed delete.
A hot key expires, and thousands of simultaneous requests all miss and hit the database at once. Mitigations:
Requests for keys that don't exist (for example IDs chosen by an attacker) always miss and always hit the database. Mitigations: cache the "not found" result briefly, validate inputs, or use a Bloom filter of existing keys.
One extremely popular key overloads the single cache node that owns it. Mitigations: an in-process L1 cache in front of Redis for that key, or replicating it under several keys (key#1…key#N) and reading one at random.
If the database can't survive the load without the cache, then a cache restart, or a cold cache after a deployment, causes an outage. Warm caches gradually, limit concurrent database fallbacks, and capacity-plan for realistic miss rates.
Good candidates:
Poor candidates:
Measure: track the hit ratio, latency, evictions and memory. A cache with a 20% hit ratio adds complexity for little gain.
Q: Why delete the cache entry on update instead of writing the new value? A: Concurrent writers can apply cache updates in a different order than their database writes, leaving an old value cached. Deleting means the next read fetches the current value. TTLs cover the remaining small race windows.
Q: How do you prevent a cache stampede on a popular key? A: Make sure only one request recomputes it (a lock, or single-flight), serve the stale value while it refreshes, refresh popular keys before they expire, and add jitter to TTLs so many keys don't expire together.
Q: In-process cache or Redis? A: In-process for tiny, extremely hot, read-mostly data where nanosecond access matters and brief inconsistency between instances is acceptable. Redis for data shared across instances, larger datasets, or anything that needs central invalidation. Many systems use both: a small L1 in-process cache in front of a Redis L2.
Q: How do you keep a cache consistent with the database? A: Perfect consistency isn't practical. Use invalidate-on-write, bounded TTLs, and for critical flows either bypass the cache or invalidate via change data capture events from the database, so no code path is forgotten.
Q: What's a good cache hit ratio? A: It depends on the workload. For hot read paths, 90%+ is common and valuable. What matters is whether the hit ratio meaningfully reduces latency and backend load compared with the cost and complexity of the cache.