Introduction
As organizations scale their Spring Boot microservices on Kubernetes, caching becomes a critical component for performance and cost optimization. While EhCache works well for single-node deployments, it introduces cache inconsistency when applications scale across multiple pods.
To solve this challenge, we can build a reusable cache library that provides:
- Pluggable cache providers
- In-memory caching using EhCache
- Distributed caching using Redis
- Provider switching through configuration
- Common cache operations exposed through a single interface
- Easy integration across all microservices
In this article, we’ll build a Spring Boot cache starter library that can be added as a Maven dependency to any microservice.
Architecture Overview
The goal is to decouple business services from specific cache implementations.
+--------------------+
| Microservice A |
+---------+----------+
|
+---------v----------+
| Cache Library |
+---------+----------+
|
+--------------+--------------+
| |
+--------v---------+ +--------v---------+
| EhCache Provider | | Redis Provider |
+------------------+ +------------------+
| |
Local Pod Memory Shared Network Cache
Benefits:
- No code changes when switching cache providers
- Centralized cache management
- Shared implementation across all microservices
- Kubernetes-ready architecture
Project Structure
cache-starter
│
├── cache-api
│ ├── CacheService.java
│ └── CacheProvider.java
│
├── cache-ehcache
│ └── EhCacheService.java
│
├── cache-redis
│ └── RedisCacheService.java
│
└── cache-autoconfiguration
└── CacheConfiguration.java
Step 1: Maven Dependencies
Cache Library pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
Step 2: Create Generic Cache Interface
package com.company.cache.api;
public interface CacheService {
<T> T get(String cacheName,
String key,
Class<T> clazz);
void put(String cacheName,
String key,
Object value);
void evict(String cacheName,
String key);
void clear(String cacheName);
boolean contains(String cacheName,
String key);
}
Step 3: EhCache Implementation
package com.company.cache.ehcache;
import com.company.cache.api.CacheService;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
public class EhCacheService implements CacheService {
private final CacheManager cacheManager;
public EhCacheService(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public <T> T get(String cacheName,
String key,
Class<T> clazz) {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
return null;
}
return cache.get(key, clazz);
}
@Override
public void put(String cacheName,
String key,
Object value) {
cacheManager.getCache(cacheName)
.put(key, value);
}
@Override
public void evict(String cacheName,
String key) {
cacheManager.getCache(cacheName)
.evict(key);
}
@Override
public void clear(String cacheName) {
cacheManager.getCache(cacheName)
.clear();
}
@Override
public boolean contains(String cacheName,
String key) {
Cache.ValueWrapper wrapper =
cacheManager.getCache(cacheName)
.get(key);
return wrapper != null;
}
}
Step 4: Redis Implementation
package com.company.cache.redis;
import com.company.cache.api.CacheService;
import org.springframework.data.redis.core.RedisTemplate;
public class RedisCacheService
implements CacheService {
private final RedisTemplate<String, Object>
redisTemplate;
public RedisCacheService(
RedisTemplate<String, Object>
redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public <T> T get(String cacheName,
String key,
Class<T> clazz) {
Object value =
redisTemplate.opsForHash()
.get(cacheName, key);
if (value == null) {
return null;
}
return clazz.cast(value);
}
@Override
public void put(String cacheName,
String key,
Object value) {
redisTemplate.opsForHash()
.put(cacheName,
key,
value);
}
@Override
public void evict(String cacheName,
String key) {
redisTemplate.opsForHash()
.delete(cacheName,
key);
}
@Override
public void clear(String cacheName) {
redisTemplate.delete(cacheName);
}
@Override
public boolean contains(String cacheName,
String key) {
return redisTemplate.opsForHash()
.hasKey(cacheName,
key);
}
}
Step 5: Auto Configuration
@Configuration
@EnableCaching
public class CacheConfiguration {
@Bean
@ConditionalOnProperty(
name = "app.cache.provider",
havingValue = "ehcache"
)
public CacheService ehCacheService(
CacheManager cacheManager) {
return new EhCacheService(cacheManager);
}
@Bean
@ConditionalOnProperty(
name = "app.cache.provider",
havingValue = "redis"
)
public CacheService redisCacheService(
RedisTemplate<String, Object>
redisTemplate) {
return new RedisCacheService(
redisTemplate);
}
}
Step 6: Publishing the Library
Publish the starter into your internal Maven repository.
<dependency>
<groupId>com.company.platform</groupId>
<artifactId>cache-starter</artifactId>
<version>1.0.0</version>
</dependency>
Every microservice only needs this dependency.
Step 7: Configure EhCache
application.properties
app.cache.provider=ehcache
spring.cache.type=jcache
spring.cache.jcache.config=classpath:ehcache.xml
ehcache.xml
<config
xmlns="http://www.ehcache.org/v3">
<cache alias="customerCache">
<key-type>java.lang.String</key-type>
<value-type>
java.lang.Object
</value-type>
<expiry>
<ttl unit="minutes">
30
</ttl>
</expiry>
<resources>
<heap unit="entries">
10000
</heap>
</resources>
</cache>
</config>
Step 8: Configure Redis
application.properties
app.cache.provider=redis
spring.redis.host=redis
spring.redis.port=6379
spring.cache.type=redis
For Redis authentication:
spring.redis.password=password
Step 9: Installing Redis Using Docker
docker run -d \
--name redis \
-p 6379:6379 \
redis:latest
Verify:
docker ps
Connect:
docker exec -it redis redis-cli
Test:
PING
Expected response:
PONG
Step 10: Deploy Redis in Kubernetes
redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7
ports:
- containerPort: 6379
redis-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
Deploy:
kubectl apply -f redis-deployment.yaml
kubectl apply -f redis-service.yaml
Step 11: Using Cache in Microservices
Inject CacheService.
@Service
public class CustomerService {
private final CacheService cacheService;
public CustomerService(
CacheService cacheService) {
this.cacheService = cacheService;
}
public Customer getCustomer(
String customerId) {
Customer customer =
cacheService.get(
"customerCache",
customerId,
Customer.class);
if(customer != null) {
return customer;
}
customer =
repository.findById(customerId)
.orElseThrow();
cacheService.put(
"customerCache",
customerId,
customer);
return customer;
}
}
Available Cache Operations
The library exposes the following methods:
cacheService.put(
cacheName,
key,
value);
cacheService.get(
cacheName,
key,
clazz);
cacheService.evict(
cacheName,
key);
cacheService.clear(
cacheName);
cacheService.contains(
cacheName,
key);
Recommended Kubernetes Strategy
Small Services
Use EhCache.
Benefits:
- No network calls
- Very fast access
- Simple deployment
Limitations:
- Cache not shared between pods
Enterprise Microservices
Use Redis.
Benefits:
- Shared cache
- Distributed architecture
- Consistent data across pods
- Horizontal scalability
- High availability support
Future Enhancements
The same library can later support:
- Hazelcast
- Apache Ignite
- Memcached
- Redis Cluster
- Redis Sentinel
- Multi-level caching (EhCache + Redis)
No changes are required in business services.
Conclusion
By introducing a reusable cache starter library, teams can standardize caching across all Spring Boot microservices. The library abstracts cache providers behind a common interface and allows seamless switching between EhCache and Redis using simple configuration changes.
This approach provides a clean migration path from single-node deployments to cloud-native Kubernetes environments while keeping application code unchanged.