Hello! Welcome to our final lesson in the module on Event-Driven Architecture.
In our previous session, we established the "why" and "how" of caching using Redis and Spring Cache. You learned about core strategies like Cache-Aside and implemented them with annotations like @Cacheable. We also highlighted the importance of setting a Time-To-Live (TTL) to manage data freshness and memory. However, this very mechanism of expiration, when combined with high traffic, can create a serious production issue.
Today, we will address that issue head-on. Your learning outcome is to explain the cache stampede problem and a strategy to mitigate it. This is a classic system design interview topic that separates candidates who only know the basics of caching from those who understand how to build resilient, production-grade systems.
1. What is a Cache Stampede?
A cache stampede (also known as a thundering herd problem) occurs when a very popular cached item expires or is evicted. At that moment, a large number of concurrent requests, which previously would have been served by the cache, all experience a cache miss simultaneously.
What happens next? They all try to regenerate the value by making a call to the primary data source—typically a database or a downstream microservice.
To understand this problem and its consequences, let's start with a short reading.
Cache Stampede Protection in Spring Boot Applications
The article 'Cache Stampede Protection in Spring Boot Applications' by Alexander Obregon provides an excellent definition of the problem. Please read the following section to get a clear picture of what a cache stampede is and why it's so dangerous.
Read the section under the heading 'How Cache Stampedes Happen'. Focus on understanding how a simple cache expiration under load can lead to a 'flood of concurrent requests'.
As the article explained, this sudden, massive spike in identical queries can overload your backend systems, leading to:
- Increased application latency: All users hitting that cache miss will experience a slow response.
- Database overload: Your database CPU and connection pool can be exhausted, slowing down the entire system.
- Cascading failures: In a microservices architecture, overwhelming one service can cause a domino effect, bringing down dependent services.
The following diagram perfectly illustrates the problem versus a potential solution.

On the left, you see the stampede in action. On the right, you see a controlled, managed refresh. Our goal for the rest of this lesson is to understand the strategies that enable this controlled behavior.
2. Strategies to Mitigate Cache Stampedes
In a system design interview, after defining the problem, you'll be expected to discuss various solutions and their trade-offs. Let's explore the most effective ones, many of which can be combined.
Strategy 1: Locking and Request Coalescing
This is one of the most robust solutions. The core idea is simple: only one thread or process gets to rebuild the cache. Everyone else waits.
- When a request results in a cache miss, it first tries to acquire a lock for that specific cache key.
- The winner: The first thread to acquire the lock is responsible for fetching the data from the database and populating the cache.
- The waiters: Other concurrent threads that miss the cache fail to acquire the lock. They wait for a short period and then retry reading from the cache, which should now be populated by the "winner" thread.
This technique is called request coalescing because it coalesces multiple requests for the same data into a single database query.
For an in-depth look at how this is implemented, please read the following section.
Cache Stampede Protection in Spring Boot Applications
This section of the 'Cache Stampede Protection' article provides excellent code examples for implementing request coalescing in both a distributed environment (using Redis with Redisson) and a single-application environment (using ConcurrentHashMap and CompletableFuture).
Read the section 'Request Coalescing'. Pay attention to the logic of tryLock in the Redis example and the use of computeIfAbsent in the ConcurrentHashMap example. These demonstrate the core 'winner-takes-all' pattern.
- Trade-offs:
- Pros: Guarantees that the backend is protected. Provides strong consistency as clients wait for the fresh data.
- Cons: Adds complexity (lock management). The "waiter" threads experience slightly higher latency as they wait for the cache to be repopulated.
Strategy 2: Stale-While-Revalidate
This strategy prioritizes low latency and high availability over immediate consistency. The idea is to serve stale data while asynchronously refreshing the cache in the background.
- A cache entry has two expiration times: a "soft" TTL (
refreshAfterWrite) and a "hard" TTL (expireAfterWrite). - When a request arrives after the soft TTL has passed but before the hard TTL, the cache immediately returns the stale value (a cache hit).
- Simultaneously, the cache triggers a single asynchronous background task to refresh the value from the database.
This ensures that users never experience the penalty of a cache miss, while still preventing a stampede because only one refresh task is kicked off.
The next section of the same article explains this with a clear Spring Boot and Caffeine example.
Cache Stampede Protection in Spring Boot Applications
Let's explore the 'Stale-While-Revalidate' pattern, which is another powerful technique for preventing stampedes.
Read the section 'Stale While Revalidate'. Focus on the difference between expireAfterWrite and refreshAfterWrite in the Caffeine cache configuration. This is a key concept.
- Trade-offs:
- Pros: Very low latency for users, as they rarely have to wait for a database query. Excellent backend protection.
- Cons: Users may be served stale data for a short period. This is unacceptable for some use cases (e.g., real-time financial transactions) but perfectly fine for others (e.g., product recommendations, user timelines).
Strategy 3: Probabilistic Approaches (Jitter and Early Expiration)
Instead of complex locking or asynchronous refreshes, you can use randomness to prevent many cache keys from expiring at the same time.
- Randomized Expiry (Jitter): Instead of a fixed TTL, add a small, random amount of time (jitter) to the expiration of each key. For a base TTL of 10 minutes, the actual TTL might be set to a random value between 10 and 11 minutes. This desynchronizes the expiration times across many keys.
- Probabilistic Early Expiration: The client fetching the data can decide to proactively refresh it before it expires based on a probability calculation. For example, if a cache item is 95% through its TTL, the client might "roll the dice" and, with a small probability, decide to refresh it.
The following reading provides code snippets for these ideas.
Cache Stampede Protection in Spring Boot Applications
These probabilistic methods are simpler to implement and can be surprisingly effective.
Read the sections 'Staggering Refresh Times in Spring Boot' and 'Randomized Expiry Windows'. Note how simple it is to add a random offset to a Redis TTL.
- Trade-offs:
- Pros: Very simple to implement. Reduces the probability of a stampede.
- Cons: Does not guarantee prevention. A stampede is still possible, just less likely.
This visual from another source nicely contrasts the impact of these strategies on database load over time.

A Note on Non-Existent Data
A related problem can occur when users repeatedly request data that doesn't exist (e.g., looking up a user with an invalid ID). If you don't cache this fact, every request will hit your database, creating a potential vector for a denial-of-service attack.
Cache Aside Pattern | System Design
This is a key interview insight. The article 'Cache Aside Pattern' offers a concise solution.
Read the subsection 'Handling Null Values'.
The solution is simple: cache the null result. Store a special value (or simply null) in the cache to indicate that the data does not exist. It's crucial to use a much shorter TTL for these "negative" cache entries so that if the data is created later, it can be fetched correctly.
Test your understanding!
You are designing a caching system for two different features on an e-commerce platform:
- Product Inventory Count: Displays the real-time stock level for a product. This data must be as accurate as possible.
- "People also bought" recommendations: A list of recommended products. This list is computationally expensive to generate.
Which cache stampede mitigation strategy would you choose for each feature, and why?
Show answer
-
Product Inventory Count:
- Strategy: Locking / Request Coalescing.
- Reasoning: Data consistency is critical here. Serving a stale inventory count could lead to overselling a product. The Stale-While-Revalidate pattern is unsuitable because it intentionally serves stale data. Locking ensures that when the inventory count cache expires, only one request hits the database to get the fresh, accurate count. While other users might experience a very brief delay, they are guaranteed to see the correct data once the cache is repopulated.
-
"People also bought" recommendations:
- Strategy: Stale-While-Revalidate.
- Reasoning: For recommendations, availability and low latency are more important than 100% up-to-the-second freshness. It's perfectly acceptable to show a user a slightly stale list of recommendations while the new list is generated in the background. The Stale-While-Revalidate pattern is ideal here because it provides an instant response to the user, ensuring a good user experience, while still protecting the backend from a stampede of expensive recommendation-generation queries.
Conclusion
You've now moved beyond basic caching to understand a critical failure mode of high-traffic systems and how to engineer for resilience. Being able to discuss the cache stampede problem and its solutions demonstrates a mature understanding of system design.
Key Takeaways:
- Cache Stampede: A concurrency problem where multiple processes simultaneously try to regenerate the same expired cache key, overwhelming the backend.
- Mitigation Strategies:
- Locking/Request Coalescing: Prioritizes consistency by allowing only one refresh at a time. Best for critical data.
- Stale-While-Revalidate: Prioritizes availability and low latency by serving stale data while refreshing asynchronously. Best for non-critical data.
- Jitter/Probabilistic Expiration: A simple way to desynchronize expirations and reduce the likelihood of a stampede.
- Combining Methods: As the
Cache Stampede Protection...article concludes, the most robust systems often layer these techniques.
Preview of the Next Lesson:
We've spent a lot of time optimizing the performance and resilience of our application code. The next step in a modern development lifecycle is to prepare our application for deployment. We are now moving into our next module: Containerization & Kubernetes.
In our next lesson, we will start this journey by learning how to create an optimized, multi-stage Dockerfile for a Spring Boot application. This will be our first step toward building portable, scalable, and cloud-native services.