Introduction
As organizations move toward microservices, caching becomes a critical component for performance, scalability, and cost optimization.
Unfortunately, many teams implement caching differently:
- Different cache providers
- Different key conventions
- Different invalidation mechanisms
- No cache metrics
- No standard configuration
This creates operational challenges and inconsistent behavior across services.
To solve this problem, platform teams can build a reusable caching library that provides:
- L1 Cache using EhCache
- L2 Cache using Redis or GemFire
- Cache Aside Pattern
- Automatic L1/L2 synchronization
- Distributed cache invalidation
- Spring Event support
- Kafka Event support
- Metrics and observability
- Simple Spring Boot integration
The goal is that every microservice only needs to:
<dependency>
<groupId>com.company.platform</groupId>
<artifactId>platform-cache-lib</artifactId>
</dependency>
and configure a few properties.
The library manages everything else.
High Level Architecture
Client
|
Controller
|
Service
|
Platform Cache Library
|
-------------------
| |
L1 Cache L2 Cache
(EhCache) (Redis/GemFire)
| |
-------------------
|
Repository
|
Database
Request Flow:
1. Check L1 (EhCache)
2. Miss?
Check L2
3. Miss?
Query Database
4. Store in L2
5. Store in L1
6. Return Data
Project Structure
platform-cache-lib
│
├── pom.xml
│
└── src/main/java
│
└── com.company.cache
│
├── annotation
│ ├── Cached.java
│
├── config
│ ├── CacheAutoConfiguration.java
│ ├── RedisConfiguration.java
│ ├── GemFireConfiguration.java
│ └── EhCacheConfiguration.java
│
├── core
│ ├── CacheManager.java
│ ├── CacheContext.java
│ └── MultiLevelCacheManager.java
│
├── strategy
│ ├── CacheProvider.java
│ ├── RedisCacheProvider.java
│ ├── GemFireCacheProvider.java
│
├── invalidation
│ ├── CacheInvalidator.java
│ ├── KafkaInvalidator.java
│ ├── SpringEventInvalidator.java
│
├── model
│ └── CacheEvent.java
│
└── metrics
└── CacheMetrics.java
Maven Dependencies
<dependencies>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-cache
</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-data-redis
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.kafka
</groupId>
<artifactId>
spring-kafka
</artifactId>
</dependency>
<dependency>
<groupId>
io.micrometer
</groupId>
<artifactId>
micrometer-core
</artifactId>
</dependency>
</dependencies>
For GemFire profile:
<dependency>
<groupId>
org.springframework.geode
</groupId>
<artifactId>
spring-geode-starter
</artifactId>
</dependency>
Strategy Pattern
The library supports multiple L2 providers.
public interface CacheProvider {
<T> T get(String key);
void put(
String key,
Object value,
long ttl);
void evict(String key);
boolean exists(String key);
}
Redis Strategy
@Component
@ConditionalOnProperty(
name = "cache.l2.type",
havingValue = "redis")
public class RedisCacheProvider
implements CacheProvider {
@Autowired
private RedisTemplate<String,Object>
redisTemplate;
@Override
public Object get(String key) {
return redisTemplate
.opsForValue()
.get(key);
}
@Override
public void put(
String key,
Object value,
long ttl) {
redisTemplate.opsForValue()
.set(
key,
value,
Duration.ofSeconds(ttl));
}
@Override
public void evict(String key) {
redisTemplate.delete(key);
}
}
GemFire Strategy
@Component
@ConditionalOnProperty(
name = "cache.l2.type",
havingValue = "gemfire")
public class GemFireCacheProvider
implements CacheProvider {
@Autowired
private Region<String,Object> region;
@Override
public Object get(String key) {
return region.get(key);
}
@Override
public void put(
String key,
Object value,
long ttl) {
region.put(key, value);
}
@Override
public void evict(String key) {
region.remove(key);
}
}
Multi-Level Cache Manager
@Service
public class MultiLevelCacheManager {
@Autowired
private CacheProvider provider;
@Autowired
private org.ehcache.Cache<String,Object>
localCache;
public <T> T get(
String key,
Supplier<T> supplier,
long ttl) {
Object local =
localCache.get(key);
if(local != null) {
return (T) local;
}
Object distributed =
provider.get(key);
if(distributed != null) {
localCache.put(
key,
distributed);
return (T) distributed;
}
T value = supplier.get();
if(value != null) {
provider.put(
key,
value,
ttl);
localCache.put(
key,
value);
}
return value;
}
}
Usage in Microservice
Service code becomes extremely simple.
@Service
public class ProductService {
@Autowired
private MultiLevelCacheManager cache;
@Autowired
private ProductRepository repository;
public Product getProduct(
String productId) {
return cache.get(
"product:" + productId,
() -> repository
.findById(productId)
.orElse(null),
300);
}
}
No Redis code.
No GemFire code.
No EhCache code.
The library handles everything.
Distributed Cache Invalidation
The biggest challenge is:
Pod1
Pod2
Pod3
If Product 1001 changes:
All L1 caches must be evicted
Invalidation Contract
public interface CacheInvalidator {
void invalidate(String key);
}
Spring Event Based Invalidation
@Component
public class SpringEventInvalidator
implements CacheInvalidator {
@Autowired
private ApplicationEventPublisher
publisher;
@Override
public void invalidate(String key) {
publisher.publishEvent(
new CacheEvent(key));
}
}
Kafka Based Invalidation
@Component
public class KafkaInvalidator
implements CacheInvalidator {
@Autowired
private KafkaTemplate<String,String>
kafkaTemplate;
@Override
public void invalidate(String key) {
kafkaTemplate.send(
"cache-events",
key);
}
}
Kafka Consumer
@KafkaListener(
topics = "cache-events")
public void consume(
String key) {
localCache.remove(key);
provider.evict(key);
}
Now all pods automatically invalidate cache.
Microservice Configuration
Redis Profile
cache.enabled=true
cache.l1.enabled=true
cache.l1.maxEntries=10000
cache.l1.ttl=300
cache.l2.type=redis
spring.redis.host=localhost
spring.redis.port=6379
cache.invalidation.type=kafka
GemFire Profile
cache.enabled=true
cache.l1.enabled=true
cache.l2.type=gemfire
gemfire.locators=
localhost[10334]
cache.invalidation.type=kafka
Auto Configuration
@Configuration
@EnableConfigurationProperties(
CacheProperties.class)
public class CacheAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MultiLevelCacheManager
cacheManager() {
return new MultiLevelCacheManager();
}
}
Microservice only adds:
<dependency>
<groupId>
com.company.platform
</groupId>
<artifactId>
platform-cache-lib
</artifactId>
</dependency>
Everything else is automatic.
Metrics and Monitoring
Expose metrics:
cache.hit.l1
cache.hit.l2
cache.miss
cache.evictions
cache.puts
cache.invalidations
Micrometer integration:
Counter.builder(
"cache.hit.l1")
.register(meterRegistry)
.increment();
Dashboard:
Grafana
|
Prometheus
|
Cache Metrics
Recommended Kubernetes Deployment
Ingress
|
Spring Boot Pods
/ | \
/ | \
EhCache EhCache EhCache
\ | /
\ | /
Redis
|
Database
Benefits:
- Fast local reads
- Shared distributed cache
- Minimal DB load
- Horizontal scalability
Future Enhancements
The framework can be extended to support:
- Hazelcast
- Ignite
- Caffeine
- Aerospike
- Multi-region replication
- Distributed locking
- Cache warming
- Cache stampede protection
- Reactive APIs
Conclusion
By standardizing caching through a reusable platform library, organizations eliminate duplicated cache implementations across microservices and establish a consistent, scalable caching strategy.
The proposed architecture provides:
- L1 EhCache for ultra-fast local reads
- L2 Redis or GemFire for distributed consistency
- Strategy Pattern for pluggable cache providers
- Event-driven invalidation
- Kafka-based synchronization
- Centralized configuration
- Production-ready observability
With a single dependency and a few configuration properties, every microservice gains enterprise-grade caching capabilities without writing cache-specific code.