Shared storage provides the source of truth. Local caches provide speed. Events help keep cache state in sync.
Project: Polar Bear Cache — GitHub repository
In my previous discussions about microservices, I explored how shared storage and event-driven synchronization can work together to manage distributed session state.
This time, I want to focus on another part of the problem: caching frequently accessed data across multiple service instances.
The Problem: Faster Reads, Distributed Cache Invalidation
In a microservices architecture, multiple service instances often need the same data, especially data that is read frequently but updated relatively infrequently.
A distributed cache can reduce database load, but accessing it still involves network round trips and potentially serialization and deserialization overhead.
So, what if each service instance could keep frequently accessed data in its own local cache, while receiving notifications when that data changes elsewhere?
That is the idea behind Polar Bear Cache, an open-source Java caching library designed to work with Spring's familiar cache annotations.
The Design: Local Cache + Event-Driven Invalidation
Each service instance maintains its own local cache. When data changes, an event service can notify other instances that specific cache entries need to be invalidated.
The key principle is simple:
Broadcast invalidation events, not the entire cached data.
Polar Bear Cache integrates with Spring's cache annotations:
@Cacheable — Returns a cached value when available, or executes the underlying method to load it when the value is missing.
@CachePut — Updates the local cache and, when event-service integration is configured, notifies other instances to invalidate the corresponding entry.
@CacheEvict — Evicts cached data locally and, when event-service integration is configured, notifies other instances to invalidate their corresponding entries.
For example, when a product is updated:
- The application updates the source of truth and its local cache.
- It publishes a cache-invalidation event through the event service.
- Other service instances receive the event and evict the corresponding local entry.
- Their next read loads the data again from the source of truth.
This allows repeated reads to stay local in the common case, while cross-instance communication is used to coordinate invalidation when data changes.
When Does This Approach Make Sense?
This design is particularly suitable for:
- Read-heavy workloads with relatively infrequent updates.
- Applications running multiple service instances, each with its own local cache.
- Use cases where brief delays in cache invalidation are acceptable.
There is an important trade-off: event-driven invalidation is asynchronous. Between a data update and the time other instances process the invalidation event, stale data may still be served.
Polar Bear Cache does not guarantee strong consistency across service instances.
Depending on the application, you may also need to consider event-delivery reliability, duplicate events, cache expiration, and recovery strategies when a service instance misses an event.
The goal is not to eliminate every consistency trade-off, but to make local caching practical in distributed applications without requiring every read to go through a remote cache.
Why “Polar Bear”?
The name comes from a somewhat optimistic engineering idea:
Local caching
→ Fewer remote cache requests and network round trips
→ Potentially more throughput from the same resources
→ Potentially fewer servers needed for a given workload
→ Lower energy consumption and greenhouse gas emissions
→ Maybe we can help save polar bears. 🐻❄️🧊
Of course, the actual impact depends on workload characteristics, infrastructure, and deployment choices. Performance improvements do not automatically translate into lower energy consumption, but I like the idea that better resource efficiency could benefit both applications and the environment.
I'd Love to Hear Your Thoughts
I'm interested in how other engineers approach local caching in distributed systems.
- How do you handle cache invalidation across service instances?
- Do you prefer event-driven invalidation, short TTLs, versioned keys, or a combination?
- What failure scenarios would you consider essential to address in this design?
If you're interested in the implementation, feedback and technical discussion are very welcome.
Less remote access, more ice. 🐻❄️🧊
Java #Spring #Microservices #Caching #DistributedSystems #OpenSource