Advanced SpringBoot Caching: How to Build a Custom CacheManager

The article explains why the default SpringBoot cache manager is insufficient for production, then walks through creating custom Caffeine and Redis CacheManager beans, configuring expiration, key prefixes, serialization, and multi‑level caching to solve issues like cache penetration, key collisions, and performance bottlenecks.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Advanced SpringBoot Caching: How to Build a Custom CacheManager

1. Spring Cache Core Mechanism and Default Pain Points

Spring Cache is not a cache component but a unified abstraction that hides the underlying implementation (JDK memory, Caffeine, Redis, Ehcache, Guava, etc.) behind annotations, allowing seamless switching.

The default cache manager has several critical drawbacks for production:

Global uniform TTL prevents fine‑grained control of different business caches.

No unified key prefix leads to easy key collisions across modules.

Redis uses JDK serialization by default, producing unreadable, non‑cross‑language data.

Cannot coexist multiple caches; local and distributed caches cannot be used together.

No eviction policy or maximum capacity, causing memory overflow.

Lacks dynamic TTL, cache monitoring, and custom eviction logic.

2. Custom Caffeine Local CacheManager

2.1 Why Not Use ConcurrentHashMap?

JDK native maps have no expiration, eviction, or LRU cleanup, and under high concurrency they easily cause memory overflow.

2.2 Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.benmanes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.3</version>
</dependency>

2.3 Configuration

import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class CaffeineCacheConfig {

    @Bean("caffeineCacheManager")
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        // Core strategy: 10‑minute expiration, max 10,000 entries
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10000)
                // Removal listener for troubleshooting
                .removalListener((key, value, cause) -> {
                    System.out.println("[Local cache eviction] key=" + key + ", cause=" + cause);
                });
        cacheManager.setCaffeine(caffeine);
        // Allow null values to prevent cache‑penetration
        cacheManager.setAllowNullValues(true);
        return cacheManager;
    }
}

2.4 Usage

@Cacheable(value = "userLocalCache", key = "#id", cacheManager = "caffeineCacheManager")
@GetMapping("/user/local/{id}")
public String getUserLocal(@PathVariable Long id) {
    System.out.println("Query database");
    return "User static data:" + id;
}

2.5 Suitable Scenarios

High‑frequency, rarely changing static data (dictionary, region, configuration).

Hot data that can tolerate brief inconsistency.

Off‑loading Redis to increase interface QPS.

3. Custom Redis Distributed CacheManager

3.1 Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 Full Configuration

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching
public class RedisCacheConfig {

    private GenericJackson2JsonRedisSerializer jsonSerializer() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        return new GenericJackson2JsonRedisSerializer(mapper);
    }

    @Bean("redisCacheManager")
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        // Global default configuration
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .prefixCacheNameWith("project:cache:")
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer()))
                .disableCachingNullValues();

        // Per‑cache TTL settings
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        configMap.put("tempCache", defaultConfig.entryTtl(Duration.ofMinutes(5)));
        configMap.put("goodsCache", defaultConfig.entryTtl(Duration.ofHours(1)));
        configMap.put("dictCache", defaultConfig.entryTtl(Duration.ofHours(24)));

        return RedisCacheManager.builder(factory)
                .cacheDefaults(defaultConfig)
                .withInitialCacheConfigurations(configMap)
                .allowInFlightCacheCreation(true)
                .build();
    }
}

3.3 Six Production Benefits

Unified key prefix avoids cross‑module collisions.

JSON serialization makes Redis data readable and cross‑language compatible.

Per‑cache TTL isolates temporary, hot, and static data lifetimes.

Null‑value caching disabled to precisely control cache‑penetration strategy.

Consistent cache style across the team reduces maintenance cost.

Dynamic cache creation allows new cache names without configuration changes.

4. Coexistence of Multiple CacheManagers

4.1 Why Multi‑Level Cache?

Pure Redis incurs network I/O, becoming a performance bottleneck for high‑QPS interfaces. A local Caffeine cache provides zero‑latency, high‑concurrency reads, while Redis ensures distributed consistency.

4.2 How It Works

SpringBoot can hold several CacheManager beans. The cacheManager attribute of @Cacheable selects the desired manager; if omitted, the bean marked @Primary is used.

4.3 Example

// Local cache: ultra‑high‑frequency static data
@Cacheable(value = "dictCache", key = "#key", cacheManager = "caffeineCacheManager")
@GetMapping("/dict/{key}")
public String getDict(@PathVariable String key) {
    return "Dictionary config";
}

// Redis cache: distributed shared data
@Cacheable(value = "goodsCache", key = "#id", cacheManager = "redisCacheManager")
@GetMapping("/goods/{id}")
public String getGoods(@PathVariable Long id) {
    return "Goods detail";
}

Conclusion

Custom CacheManagers transform a simple @Cacheable usage into a sophisticated caching architecture with multi‑level storage, differentiated TTLs, standardized keys, and high performance, effectively solving common production problems such as cache penetration, key conflicts, performance bottlenecks, and memory overflow.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceCacheRedisCaffeineSpringBootMulti-level CacheCacheManager
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

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.