Introduction
Caching is one of the most effective ways to improve application performance. Whether you are using EhCache, Redis, GemFire, or a multi-level caching strategy, one challenge remains consistent across all distributed systems:
How do you ensure that stale data is not served from cache?
Reading from a cache is easy. Maintaining cache consistency across multiple pods, services, and clusters is the difficult part.
In this article, we will explore:
- Why cache invalidation is hard
- Cache invalidation strategies
- Spring Event based invalidation
- Kafka based invalidation
- Redis Pub/Sub based invalidation
- L1 and L2 cache synchronization
- Recommended enterprise architecture
By the end of this article, you’ll understand how to keep distributed caches consistent across Kubernetes-based Spring Boot microservices.
The Problem
Consider a typical microservice deployed in Kubernetes.
Product Service
Pod-1
Pod-2
Pod-3
L1 Cache = EhCache
L2 Cache = Redis
Assume all pods have cached:
product:1001
Current value:
Price = 1000
Stored in:
Pod-1 EhCache
Pod-2 EhCache
Pod-3 EhCache
Redis
Database
Now a user updates the product.
PUT /products/1001
New value:
Price = 1200
Database update succeeds.
Question:
How do all application instances know that the cached value is no longer valid?
Why Cache Invalidation is Hard
Imagine the following state:
Database = 1200
Redis = 1000
Pod1 EhCache = 1000
Pod2 EhCache = 1000
Pod3 EhCache = 1000
Even though the database contains the latest value, users may continue receiving stale responses from cache.
This problem becomes increasingly difficult as:
- Number of pods grows
- Number of services grows
- Number of cache layers grows
Common Cache Invalidation Strategies
There is no one-size-fits-all solution.
Most organizations use one or more of the following strategies.
Strategy 1: TTL Based Invalidation
The simplest approach.
Every cache entry has an expiration time.
Example:
Product Cache = 5 Minutes
Customer Cache = 30 Minutes
Reference Data = 24 Hours
Configuration:
cache.put(
key,
value,
Duration.ofMinutes(5)
);
Advantages:
- Very simple
- No messaging required
- Easy to implement
Disadvantages:
- Stale data exists until TTL expires
- Not suitable for frequently changing data
Best for:
- Reference data
- Configuration data
- Product catalogs
Strategy 2: Event Based Invalidation
Whenever data changes:
Update Database
↓
Invalidate Cache
Advantages:
- Immediate consistency
- Minimal stale data
Disadvantages:
- Requires messaging or event propagation
Best for:
- Product updates
- Customer profiles
- Inventory systems
Strategy 3: Write Through Cache
Update cache and database simultaneously.
Application
↓
Cache
↓
Database
Advantages:
- Cache always synchronized
Disadvantages:
- Increased write latency
Best for:
- Frequently read data
- Moderate write workloads
Strategy 4: Write Behind Cache
Update cache immediately.
Persist database asynchronously.
Application
↓
Cache
↓
Async Queue
↓
Database
Advantages:
- Extremely fast writes
Disadvantages:
- Data loss risk during failures
Best for:
- High throughput systems
Strategy 5: Cache Versioning
Store version information with cache entries.
Example:
product:1001:v1
product:1001:v2
When data changes:
Increase Version
Old cache entries become obsolete.
Advantages:
- No explicit cache deletion required
Disadvantages:
- Increased memory usage
Best for:
- Large distributed systems
Spring Event Based Invalidation
Spring Framework provides an event publishing mechanism.
Architecture:
ProductService
↓
ApplicationEventPublisher
↓
Spring Event Listener
Publishing Event
@Service
public class ProductService {
@Autowired
private ProductRepository repository;
@Autowired
private ApplicationEventPublisher publisher;
public Product update(Product product) {
Product updated =
repository.save(product);
publisher.publishEvent(
new CacheInvalidationEvent(
"product:" + product.getId()));
return updated;
}
}
Event Listener
@Component
public class CacheEventListener {
@Autowired
private EhCacheManager cacheManager;
@EventListener
public void invalidate(
CacheInvalidationEvent event) {
cacheManager.remove(
event.getCacheKey());
}
}
Important Limitation
Spring Events are JVM local.
Example:
Pod-1
Publish Event
↓
Pod-1 Receives Event
But:
Pod-2
No Event
Pod-3
No Event
Result:
Pod-1 Cache Cleared
Pod-2 Stale
Pod-3 Stale
Spring Events do not cross pod boundaries.
When Spring Events Make Sense
Use Spring Events when:
Single Application
Single JVM
Monolithic Application
or
Invalidate Local L1 Cache
Update Metrics
Refresh Local State
Kafka Based Invalidation
Kafka is the most common enterprise solution.
Architecture:
Kafka
|
-----------------------------
| | |
Pod1 Pod2 Pod3
Every pod subscribes to cache invalidation events.
Update Flow
Step 1
Update Product
repository.save(product);
Step 2
Publish Event
kafkaTemplate.send(
"cache-events",
"product:1001");
Step 3
Kafka Stores Event
Topic
cache-events
Step 4
All Pods Consume Event
Pod1
Pod2
Pod3
Step 5
Clear Local L1 Cache
@KafkaListener(
topics = "cache-events")
public void invalidate(
String key) {
ehCache.remove(key);
}
Result:
Pod1 Cleared
Pod2 Cleared
Pod3 Cleared
What About Redis?
Many engineers assume Kafka must invalidate Redis too.
In reality:
Redis
Shared By All Pods
There is only one distributed cache.
Therefore:
redisTemplate.delete(
"product:1001");
Immediately removes the cache entry.
All pods automatically see:
Redis MISS
No Kafka needed for Redis invalidation itself.
Kafka is only required to invalidate L1 caches.
Complete Enterprise Flow
Before Update
Pod1 L1 = 1000
Pod2 L1 = 1000
Pod3 L1 = 1000
Redis = 1000
Database = 1000
Update Product
Price = 1200
Step 1
Update Database
Database = 1200
Step 2
Evict Redis
redisTemplate.delete(
"product:1001");
Redis becomes:
MISS
Step 3
Publish Kafka Event
{
"cacheKey":"product:1001"
}
Step 4
All Pods Consume Event
Pod1 Clear L1
Pod2 Clear L1
Pod3 Clear L1
Final State
Pod1 L1 Empty
Pod2 L1 Empty
Pod3 L1 Empty
Redis Empty
Database 1200
Next Request
L1 Miss
↓
Redis Miss
↓
Database Read
↓
Populate Redis
↓
Populate L1
Fresh data now flows through the entire cache hierarchy.
Redis Pub/Sub Based Invalidation
Redis also provides Pub/Sub capabilities.
Architecture:
Redis Publisher
↓
Redis Channel
↓
All Application Pods
Publishing Event
redisTemplate.convertAndSend(
"cache-events",
"product:1001");
Listener
public void onMessage(
Message message) {
ehCache.remove(
message.toString());
}
Advantages
- Simpler than Kafka
- Fewer moving parts
- Easy setup
Disadvantages
- Messages are not durable
- Events can be lost during restarts
- No replay capability
Comparing Invalidation Approaches
| Strategy | Multi-Pod Support | Durable | Replay Support | Complexity |
|---|---|---|---|---|
| TTL | Yes | N/A | N/A | Low |
| Spring Events | No | No | No | Low |
| Redis Pub/Sub | Yes | No | No | Medium |
| Kafka Events | Yes | Yes | Yes | Medium |
| Cache Versioning | Yes | Yes | Yes | High |
Recommended Architecture for Modern Microservices
For Kubernetes-based Spring Boot platforms:
L1 Cache = EhCache
L2 Cache = Redis/GemFire
Invalidation = Kafka
Architecture:
Kafka
|
-----------------------------
| | |
Pod1 Pod2 Pod3
| | |
EhCache EhCache EhCache
\ | /
\ | /
Redis
|
Database
Update Flow:
Update Service
↓
Save Database
↓
Evict Redis
↓
Publish Kafka Event
↓
All Pods Evict L1
↓
Next Read Rebuilds Cache
Benefits:
- Works across pods
- Works across clusters
- Durable event delivery
- Replay capability
- Cloud native
- Enterprise ready
How a Platform Cache Library Should Handle It
Microservice developers should not worry about cache synchronization.
Instead of writing:
redis.delete(...);
kafka.send(...);
ehCache.remove(...);
The service simply calls:
cacheManager.invalidate(
"product:" +
product.getId());
The platform library internally:
1. Evicts L2 Cache
2. Publishes Kafka Event
3. Clears Local L1
4. Other Pods Receive Event
5. Other Pods Clear L1
This keeps caching concerns centralized and consistent across all microservices.
Conclusion
Cache invalidation is one of the most challenging aspects of distributed system design. While TTL-based approaches are easy to implement, event-driven invalidation provides stronger consistency and better user experience.
For modern Spring Boot microservices running on Kubernetes, a multi-level cache architecture combined with Kafka-based invalidation offers the best balance of performance, scalability, and consistency.
A platform-managed cache library can abstract these complexities away from application teams, enabling developers to focus on business logic while ensuring cache consistency across the entire platform.