Backend Development 7 min read

Boost Application Performance with Multi‑Level Caffeine & Redis Caching

This article explains why multi‑level caching is needed, outlines design challenges of combining Redis and Caffeine, provides step‑by‑step integration instructions for Spring Boot, and presents performance benchmarks demonstrating the speed advantage of the multi‑level approach.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
Boost Application Performance with Multi‑Level Caffeine & Redis Caching

Why Multi‑Level Cache

Cache is essential for modern systems; as data size and structure grow, Redis alone can suffer performance degradation and network I/O becomes a significant bottleneck, especially in micro‑service architectures where a single request may trigger multiple downstream calls.

Design Challenges

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

Data consistency between cache layers (e.g., JVM‑level Caffeine and distributed Redis) is difficult to guarantee.

Spring Cache lacks active expiration policies, making cache eviction management cumbersome.

Business Flow

How to Use

Add the starter 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.1.0&lt;/version&gt;
&lt;/dependency&gt;</code>

Enable caching in the Spring Boot application:

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

Declare cacheable methods 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 on localhost.

Benchmark      Mode   Cnt    Score   Units
Multi‑level    thrpt   2   2716.074 ops/s
Default Redis  thrpt   2   1373.476 ops/s

Code Principles

Custom

CacheManager

that creates a combined Redis‑Caffeine cache instance.

<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>

Cache read logic checks Caffeine first, then falls back to Redis, and populates Caffeine on a Redis hit.

<code>protected Object lookup(Object key) {
    Object cacheKey = getKey(key);
    Object value = caffeineCache.getIfPresent(key);
    if (value != null) {
        log.debug("get cache from caffeine, the key is : {}", cacheKey);
        return value;
    }
    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;
}</code>

All write and eviction operations publish a Redis Pub/Sub message so that other nodes can invalidate their local Caffeine entries.

<code>public void put(Object key, Object value) {
    push(new CacheMessage(this.name, key));
}
public void evict(Object key) {
    push(new CacheMessage(this.name, key));
}
private void push(CacheMessage message) {
    stringKeyRedisTemplate.convertAndSend(topic, message);
}</code>

Message listener removes the corresponding entry from the local Caffeine cache when a Pub/Sub notification is received.

<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

RediscachingSpring 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.