Building a Pluggable Multi-Level Cache Library for Spring Boot Microservices (EhCache + Redis/GemFire)

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.

Leave a Reply

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