How CacheEvictedEventBus Enables Distributed Cache Invalidation in Cocache

The CacheEvictedEventBus is a lightweight publish/subscribe mechanism that broadcasts cache eviction events across JVMs, ensuring coherent caches stay synchronized by invalidating stale entries locally when remote modifications occur.

In the ahoo-wang/cocache repository, the CacheEvictedEventBus serves as the backbone for maintaining consistency across distributed cache instances. When one node modifies a cache entry, this event bus propagates CacheEvictedEvent messages to all registered subscribers, triggering local invalidation without requiring direct remote calls per operation.

Core Architecture

The distributed invalidation system revolves around three primary components defined in the cocache-core module.

CacheEvictedEventBus Interface

The contract is defined in cocache-core/src/main/kotlin/me/ahoo/cache/consistency/CacheEvictedEventBus.kt and specifies three primitive operations:

  • publish(event: CacheEvictedEvent) – Broadcasts an eviction event to all subscribers
  • register(subscriber: CacheEvictedSubscriber) – Adds a subscriber to receive future events
  • unregister(subscriber: CacheEvictedSubscriber) – Removes a subscriber from the bus

CacheEvictedEvent Payload

The event structure in CacheEvictedEvent.kt carries the minimal data required for invalidation:

  • cacheName – Identifies which cache instance should evict
  • key – The specific cache key to invalidate
  • publisherId – Unique identifier of the originating node (prevents self-eviction loops)

CacheEvictedSubscriber Interface

Implementations of CacheEvictedSubscriber (such as DefaultCoherentCache) provide the onEvicted(event: CacheEvictedEvent) method to handle incoming invalidation requests.

Distributed Invalidation Flow

When a cache entry is modified, the CacheEvictedEventBus orchestrates consistency through the following sequence:

  1. Local MutationDefaultCoherentCache.setCache() or evict() updates the client-side and distributed cache layers, then calls cacheEvictedEventBus.publish().

  2. Event Propagation

    • Guava implementation: eventBus.post(event) notifies in-process subscribers instantly
    • Redis implementation: redisTemplate.convertAndSend(channel, message) publishes to a Redis Pub/Sub channel named after the cache
  3. Remote Reception – Each CacheEvictedSubscriber receives the event via:

    • Guava's @Subscribe annotation adapter
    • Redis MessageListenerAdapter that deserializes the message back to CacheEvictedEvent
  4. Local EvictionDefaultCoherentCache.onEvicted() validates the event by checking:

    • The cacheName matches the local cache
    • The publisherId differs from the local node ID (avoiding self-eviction)
    • If valid, calls clientSideCache.evict(key) to remove the stale entry

Implementation Strategies

The repository provides two concrete implementations of the CacheEvictedEventBus interface to support different deployment topologies.

GuavaCacheEvictedEventBus

Located in cocache-core/src/main/kotlin/me/ahoo/cache/consistency/GuavaCacheEvictedEventBus.kt, this implementation wraps Google's EventBus for single-process or testing environments. It provides synchronous, in-memory event dispatch without external dependencies.

RedisCacheEvictedEventBus

Found in cocache-spring-redis/src/main/kotlin/me/ahoo/cache/spring/redis/RedisCacheEvictedEventBus.kt, this production-ready implementation leverages Redis Pub/Sub for cross-process communication. Each subscriber creates a MessageListenerAdapter that converts Redis messages back into CacheEvictedEvent objects, enabling cache coherence across distributed microservices.

Wiring Components Together

The DefaultCoherentCacheFactory in cocache-core/src/main/kotlin/me/ahoo/cache/consistency/DefaultCoherentCacheFactory.kt injects the chosen CacheEvictedEventBus implementation into DefaultCoherentCache instances during creation. This factory pattern ensures that every coherent cache automatically participates in the distributed invalidation protocol without manual wiring.

Practical Examples

Guava-Based Event Bus for Unit Testing

import me.ahoo.cache.consistency.*

// Create the in-process bus
val evictedBus = GuavaCacheEvictedEventBus()

// Register a subscriber
evictedBus.register(object : CacheEvictedSubscriber {
    override fun onEvicted(event: CacheEvictedEvent) {
        println("Received eviction: ${event.cacheName}:${event.key}")
    }
})

// Publish an event from producer node "app-01"
evictedBus.publish(CacheEvictedEvent("userCache", "user:123", "app-01"))

Redis-Backed Production Configuration

import org.springframework.data.redis.core.StringRedisTemplate
import org.springframework.data.redis.listener.RedisMessageListenerContainer
import me.ahoo.cache.spring.redis.RedisCacheEvictedEventBus

@Bean
fun cacheEvictedEventBus(
    redisTemplate: StringRedisTemplate,
    listenerContainer: RedisMessageListenerContainer
): CacheEvictedEventBus {
    return RedisCacheEvictedEventBus(redisTemplate, listenerContainer)
}

// Automatic invalidation occurs when:
// myCoherentCache.setCache("product:456", cacheValue) 
// This triggers publish() behind the scenes

Factory-Based Creation

val bus: CacheEvictedEventBus = RedisCacheEvictedEventBus(redisTemplate, listenerContainer)
val factory = DefaultCoherentCacheFactory(bus)

val coherentCache = factory.create(
    cacheName = "orderCache",
    clientSideCache = caffeineCache,
    distributedCache = redisCache,
    cacheSource = orderCacheSource
)

Summary

  • CacheEvictedEventBus provides the publish/subscribe contract for distributed invalidation through publish(), register(), and unregister() methods
  • CacheEvictedEvent carries cacheName, key, and publisherId to target specific entries while avoiding self-eviction loops
  • DefaultCoherentCache automatically publishes events on write operations and handles remote invalidations via onEvicted()
  • GuavaCacheEvictedEventBus supports single-process deployments using Google's EventBus
  • RedisCacheEvictedEventBus enables cross-JVM coherence using Redis Pub/Sub channels
  • The DefaultCoherentCacheFactory wires the event bus into cache instances automatically

Frequently Asked Questions

How does CacheEvictedEventBus prevent a node from evicting its own cache entries?

Each CacheEvictedEvent includes a publisherId field containing the unique identifier of the originating node. When DefaultCoherentCache.onEvicted() receives an event, it compares the event's publisherId against the local node ID. If they match, the event is ignored, preventing self-eviction while still processing events from other nodes.

What is the difference between GuavaCacheEvictedEventBus and RedisCacheEvictedEventBus?

GuavaCacheEvictedEventBus uses Google's in-memory EventBus and only works within a single JVM process, making it suitable for testing or monolithic deployments. RedisCacheEvictedEventBus publishes events to Redis Pub/Sub channels, allowing multiple JVM processes or microservices to subscribe to invalidation events and maintain cache coherence across distributed systems.

Which source files define the core CacheEvictedEventBus contract?

The interface is defined in cocache-core/src/main/kotlin/me/ahoo/cache/consistency/CacheEvictedEventBus.kt. Supporting classes include CacheEvictedEvent.kt (payload), CacheEvictedSubscriber.kt (consumer interface), and DefaultCoherentCache.kt (publisher/subscriber implementation). Redis-specific implementation resides in cocache-spring-redis/src/main/kotlin/me/ahoo/cache/spring/redis/RedisCacheEvictedEventBus.kt.

How is CacheEvictedEventBus integrated with Spring Boot applications?

The CoCacheAutoConfiguration class in the Spring Boot starter automatically configures a RedisCacheEvictedEventBus when Spring Data Redis is detected on the classpath. The DefaultCoherentCacheFactory then injects this bean into all coherent cache instances, enabling distributed invalidation without explicit configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →