Cache Invalidation in Distributed Spring Boot Microservices: Spring Events vs Kafka vs Redis Pub/Sub

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

StrategyMulti-Pod SupportDurableReplay SupportComplexity
TTLYesN/AN/ALow
Spring EventsNoNoNoLow
Redis Pub/SubYesNoNoMedium
Kafka EventsYesYesYesMedium
Cache VersioningYesYesYesHigh

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.

Leave a Reply

Your email address will not be published. Required fields are marked *