Backend Development 6 min read

Boost Spring Boot Performance with Multi‑Level Caffeine‑Redis Cache

This article explains why a multi‑level cache combining JVM‑level Caffeine and distributed Redis is essential for modern Spring Boot applications, outlines design challenges, provides step‑by‑step integration instructions, and demonstrates performance gains with benchmark results and core implementation code.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
Boost Spring Boot Performance with Multi‑Level Caffeine‑Redis Cache

Why Multi‑Level Cache

Introducing caching is now a must for most systems. While Redis is a common middleware, large data sizes and complex structures can degrade performance, and network I/O becomes a significant bottleneck, especially in micro‑service architectures where a single request may trigger many downstream calls.

Caffeine, a high‑performance local in‑memory cache, offers considerably better speed than typical memory cache implementations.

Conclusion: we need to build an L1 Caffeine JVM‑level cache and an L2 Redis cache.

Design Challenges

Most application caches are based on Spring Cache with annotation support, which has the following limitations:

Spring Cache supports only a single cache provider, so Redis and Caffeine cannot be used simultaneously.

Data consistency issues between cache layers (e.g., application‑level cache vs. distributed cache).

Spring Cache does not support proactive expiration policies.

How to Use

Add the dependency:

<code>&lt;dependency&gt;
    &lt;groupId&gt;com.pig4cloud.plugin&lt;/groupId&gt;
    &lt;artifactId&gt;multilevel-cache-spring-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;0.0.1&lt;/version&gt;
&lt;/dependency&gt;</code>

Enable cache support:

<code>@EnableCaching
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}</code>

Declare target interface with Spring Cache annotations:

<code>@Cacheable(value = "get", key = "#key")
@GetMapping("/get")
public String get(String key) {
    return "success";
}</code>

Performance Comparison

Benchmark environment: macOS Mojave, 2.3 GHz Intel Core i5, 8 GB RAM, Corretto 11 JDK, Redis running locally.

Results (operations per second):

Multi‑level implementation: 2716 ops/s

Default Redis only: 1373 ops/s

Code Principles

Custom CacheManager for Multi‑Level Cache

<code>public class RedisCaffeineCacheManager implements CacheManager {
    @Override
    public Cache getCache(String name) {
        Cache cache = cacheMap.get(name);
        if (cache != null) {
            return cache;
        }
        cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(), cacheConfigProperties);
        Cache oldCache = cacheMap.putIfAbsent(name, cache);
        log.debug("create cache instance, the cache name is : {}", name);
        return oldCache == null ? cache : oldCache;
    }
}</code>

Multi‑Level Read and Expiration Strategy

<code>public class RedisCaffeineCache extends AbstractValueAdaptingCache {
    @Override
    protected Object lookup(Object key) {
        Object cacheKey = getKey(key);
        // 1. Try Caffeine first
        Object value = caffeineCache.getIfPresent(key);
        if (value != null) {
            log.debug("get cache from caffeine, the key is : {}", cacheKey);
            return value;
        }
        // 2. Fallback to Redis
        value = stringKeyRedisTemplate.opsForValue().get(cacheKey);
        if (value != null) {
            log.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
            caffeineCache.put(key, value);
        }
        return value;
    }

    @Override
    public void put(Object key, Object value) {
        push(new CacheMessage(this.name, key));
    }

    @Override
    public void evict(Object key) {
        push(new CacheMessage(this.name, key));
    }

    @Override
    public void clear() {
        push(new CacheMessage(this.name, null));
    }

    private void push(CacheMessage message) {
        stringKeyRedisTemplate.convertAndSend(topic, message);
    }
}</code>

Message Listener for Cache Invalidation

<code>public class CacheMessageListener implements MessageListener {
    private final RedisTemplate<Object, Object> redisTemplate;
    private final RedisCaffeineCacheManager redisCaffeineCacheManager;

    @Override
    public void onMessage(Message message, byte[] pattern) {
        CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());
        redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey());
    }
}</code>

Source Code

https://github.com/pig-mesh/multilevel-cache-spring-boot-starter

References

pig oauth2.0 client authentication: https://gitee.com/log4j/pig

Caffeine benchmark details: https://github.com/ben-manes/caffeine/wiki/Benchmarks

PerformanceCacheRedisSpring BootCaffeineMultilevel Cache
Java Architecture Diary
Written by

Java Architecture Diary

Committed to sharing original, high‑quality technical articles; no fluff or promotional content.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.