Project: Caching Patterns
Introduction
Caching is the most common Redis use case, but naive caching creates problems at scale. This chapter walks through three classic failure modes—cache penetration, cache breakdown, and cache avalanche—and implements proven defenses. You will build a Spring Boot service that caches product details safely, using real techniques such as null-value caching, mutual exclusion locks, and probabilistic early expiration.
Prerequisites
- Completion of Spring Boot Integration
- A running Redis server and a Spring Boot project with
spring-boot-starter-data-redis - Basic understanding of cache hit and miss concepts
The Three Cache Failure Modes
Cache Penetration
A request asks for data that does not exist in the database. Without protection, every request passes through Redis and hammers the database.
Defense: Cache the absence. Store a short-lived placeholder (for example, an empty JSON object or the string "__NULL__") so repeated requests for the same missing key stop at Redis.
Cache Breakdown
A single hot key expires. Thousands of concurrent requests race to the database to regenerate it, overwhelming that one query.
Defense: Use a mutex. Only the first requester rebuilds the value; others wait or retry from Redis.
Cache Avalanche
Many keys expire at the same instant (for example, after a bulk import with identical TTLs). The database faces a thundering herd of regeneration queries.
Defense: Add jitter. Randomize expiration times so keys do not vanish simultaneously.
Building a Defensive Cache Service
The Product Model
import java.io.Serializable;
import java.math.BigDecimal;
public record Product(
Long id,
String name,
String description,
BigDecimal price,
Integer stock
) implements Serializable {
}The Repository
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}The Cache Service with All Defenses
The Controller
Cache-Aside vs Write-Through vs Write-Behind
The implementation above uses cache-aside: the application checks Redis first, falls back to the database, and populates Redis itself.
| Pattern | When Redis Is Updated | Complexity | Consistency |
|---|---|---|---|
| Cache-aside | On read miss or explicit invalidation | Low | Eventual |
| Write-through | On every database write | Medium | Strong |
| Write-behind | Asynchronously after database write | High | Eventual, risk of loss |
Cache-aside is the most common pattern because it is simple and resilient. Write-through is useful when stale reads are unacceptable, but it adds latency to every write.
Testing the Defenses
Penetration Test
# Request a non-existent product 1000 times
for i in {1..1000}; do
curl -s http://localhost:8080/products/99999 > /dev/null
doneWith null-value caching, only the first request hits the database. The remaining 999 are blocked by the __NULL__ placeholder in Redis.
Breakdown Test
Use a load-testing tool such as wrk or ab against a single product key. Allow the key to expire while traffic is running. The mutex ensures that only one request regenerates the value; the rest wait briefly and then read from the refreshed cache.
FAQ
How long should I cache null values?
Short—typically 1 to 5 minutes. You want to block the thundering herd without permanently hiding newly created data.
What if the mutex holder crashes before releasing the lock?
The setIfAbsent call in the example includes a 10-second expiration. Even if the JVM crashes, Redis removes the lock automatically. Tune this timeout to be longer than your slowest database query.
Can I use Spring Cache annotations instead of manual caching?
Yes, for simple cases. However, Spring Cache does not natively handle null-value caching, mutex-based breakdown protection, or TTL jitter. For high-traffic endpoints, the manual approach shown here gives you full control.
Should I use a Bloom filter instead of null-value caching?
Bloom filters are excellent for penetration prevention when the universe of invalid keys is enormous (for example, user-uploaded content IDs). For a typical e-commerce catalog, null-value caching is simpler and sufficient. If your database has hundreds of millions of products and bots probe random IDs constantly, consider adding a Bloom filter as a first line of defense.
Redis with MongoDB instead of MySQL?
Same cache-aside pattern: MongoDB remains source of truth; Redis holds hot documents or API responses. See MongoDB + Redis deploy and MySQL vs MongoDB for when each store fits.