Building a Pluggable Distributed Cache Library for Spring Boot Microservices Using EhCache and Redis

gof

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.

Leave a Reply

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