Caching Strategies
BeginnerCaching stores frequently accessed data in a fast layer (memory) to reduce latency and database load. Strategies include cache-aside, read-through, write-through, write-behind, and refresh-ahead.
Overview
Caching is the single most impactful technique for improving system performance. A cache stores a subset of data in a faster storage layer (typically in-memory like Redis or Memcached) so that future requests can be served without hitting the slower origin (database, API, disk). The key challenges are cache invalidation (when does stale data get replaced?), cache eviction (what gets removed when the cache is full?), and cache consistency (how closely does cached data match the source of truth?). Different caching strategies — cache-aside (lazy loading), read-through, write-through, write-behind (write-back), and refresh-ahead — offer different trade-offs between consistency, performance, and complexity.
Cache-Aside (Lazy Loading)
The application checks the cache first. On a miss, it reads from the DB, stores the result in the cache, and returns it. On a write, the application updates the DB and invalidates (or updates) the cache entry. This is the most common pattern.
// Cache-aside with Redis + Spring Boot
@Service
public class ProductService {
private final RedisTemplate<String, Product> redis;
private final ProductRepository repo;
public Product getProduct(String id) {
String key = "product:" + id;
// 1. Check cache
Product cached = redis.opsForValue().get(key);
if (cached != null) return cached; // cache HIT
// 2. Cache miss → read from DB
Product product = repo.findById(id)
.orElseThrow(() -> new NotFoundException(id));
// 3. Populate cache with TTL
redis.opsForValue().set(key, product, Duration.ofMinutes(30));
return product;
}
public Product updateProduct(String id, ProductUpdateDTO dto) {
Product updated = repo.save(/* ... */);
redis.delete("product:" + id); // invalidate cache
return updated;
}
}Write-Through & Write-Behind
Write-through writes to cache and DB synchronously — ensures consistency but adds write latency. Write-behind (write-back) writes to cache immediately and asynchronously flushes to DB — faster writes but risk of data loss if cache crashes before flush.
// Write-through: cache + DB updated together
// App → Cache → DB (synchronous, consistent)
// Write-behind: cache updated, DB updated async
// App → Cache → [async queue] → DB (fast, risk of loss)
// Comparison
// ─────────────────────────────────────────────────────
// Strategy | Read perf | Write perf | Consistency
// ─────────────────────────────────────────────────────
// Cache-aside | Fast (hit)| Normal | Eventual
// Read-through | Fast (hit)| Normal | Eventual
// Write-through | Fast (hit)| Slower | Strong
// Write-behind | Fast (hit)| Fast | Eventual (risk)
// Refresh-ahead | Fast | Normal | Near real-time
// Spring @Cacheable (read-through abstraction)
@Cacheable(value = "products", key = "#id")
public Product getProduct(String id) {
return repo.findById(id).orElseThrow();
}
@CacheEvict(value = "products", key = "#id")
public void updateProduct(String id, ProductDTO dto) {
repo.save(/* ... */);
}Eviction Policies & TTL
When the cache is full, eviction policies decide what to remove. LRU (Least Recently Used) is the most common. TTL (Time-To-Live) ensures entries expire after a fixed duration, bounding staleness.
// Common eviction policies:
// LRU — evict least recently used (most popular)
// LFU — evict least frequently used
// FIFO — evict oldest entry
// Random — evict a random entry
// Redis: set max memory + eviction policy
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
// Caffeine (Java in-process cache) with LRU + TTL
Cache<String, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000) // LRU eviction at 10K entries
.expireAfterWrite(Duration.ofMinutes(15)) // TTL
.recordStats() // hit/miss metrics
.build();
// Multi-layer caching: L1 (in-process) → L2 (Redis) → DB
// L1 = Caffeine (ns latency), L2 = Redis (ms latency), DB (10s of ms)Key Points to Remember
- 1Cache-aside (lazy loading) is the most common strategy — app manages cache reads and invalidation.
- 2Write-through ensures consistency but adds write latency; write-behind is faster but risks data loss.
- 3LRU is the default eviction policy in most caches; always set a TTL to bound staleness.
- 4Multi-layer caching (L1 in-process + L2 distributed) combines low latency with shared invalidation.
- 5Cache invalidation is one of the hardest problems — prefer TTL-based expiry with event-driven invalidation.
Interview Questions
Sign in to ask AriaWhat are the common caching strategies and when would you use each?
How do you handle cache invalidation in a microservices architecture?
What is the thundering herd problem and how do you solve it?
Compare Redis vs Memcached for a session cache.
Design a multi-layer caching strategy for a product catalog with 10M items.
Ask Aria about Caching Strategies
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.