Cache Warm‑up and Hierarchical Caching in Spring Boot: Design and Implementation

This article explains how to improve Spring Boot API response speed by designing a cache‑warm‑up mechanism, hierarchical caching with local and Redis layers, and a functional‑programming template for cache‑first queries, providing complete Java code examples and integration guidelines.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Cache Warm‑up and Hierarchical Caching in Spring Boot: Design and Implementation

1. Introduction

Hello, I am sum墨, a backend Java developer with six years of experience. I have built multi‑tenant systems, integrated open platforms, and created complex message centers, but have never suffered data loss due to code crashes. This article focuses on practical cache techniques to boost interface performance, not just theory.

Cache performance can be improved through index creation, SQL optimization, adding caches, and refactoring. Here we concentrate on using caches effectively, covering cache warm‑up, hierarchical caching, and hot‑data caching.

2. Cache Warm‑up: Building a Cache Processor

We typically use ConcurrentHashMap or Redis to store temporary data and @Scheduled for periodic refresh. While this works for simple scenarios, it becomes cumbersome as the number of warm‑up items grows, so a dedicated cache processor is needed.

2.1 Cache Processor Design

DAL implementation producing DAO and DO objects, defining cache domain models.

Define cache names with attention to initialization order.

Write data repositories and model converters to transform data models to cache models.

Implement a cache manager, preferably extending AbstractCacheManager.

Design basic cache APIs (putAll, get, getCacheInfo, etc.) according to business needs.

Provide bean configuration for plug‑in registration of managers, repositories, and extension points.

2.2 Code Implementation

a. CacheNameDomain Interface

package com.summo.demo.cache;

public interface CacheNameDomain {
    /**
     * Cache initialization order, lower value means earlier initialization.
     */
    int getOrder();
    /**
     * Cache name, preferably uppercase English letters.
     */
    String getName();
    /**
     * Description for logging.
     */
    String getDescription();
}

b. CacheNameEnum

package com.summo.demo.cache;

import org.springframework.core.Ordered;

public enum CacheNameEnum implements CacheNameDomain {
    SYS_CONFIG("SYS_CONFIG", "System configuration cache", Ordered.LOWEST_PRECEDENCE);
    
    private String name;
    private String description;
    private int order;
    
    CacheNameEnum(String name, String description, int order) {
        this.name = name;
        this.description = description;
        this.order = order;
    }
    
    @Override
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    @Override
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    @Override
    public int getOrder() { return order; }
    public void setOrder(int order) { this.order = order; }
}

c. CacheMessageUtil

package com.summo.demo.cache;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

public final class CacheMessageUtil {
    private static final char ENTERSTR = '
';
    private static final char MAP_EQUAL = '=';
    private CacheMessageUtil() { }
    public static String toString(List<?> cacheDatas) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < cacheDatas.size(); i++) {
            builder.append(cacheDatas.get(i));
            if (i != cacheDatas.size() - 1) builder.append(ENTERSTR);
        }
        return builder.toString();
    }
    public static String toString(Map<?, ?> map) {
        StringBuilder builder = new StringBuilder();
        int count = map.size();
        for (Iterator<?> i = map.keySet().iterator(); i.hasNext();) {
            Object name = i.next();
            builder.append(name).append(MAP_EQUAL).append(map.get(name));
            if (count-- != 1) builder.append(ENTERSTR);
        }
        return builder.toString();
    }
}

d. CacheManager Interface

package com.summo.demo.cache;

import org.springframework.core.Ordered;

public interface CacheManager extends Ordered {
    void initCache();
    void refreshCache();
    CacheNameDomain getCacheName();
    void dumpCache();
    long getCacheSize();
}

e. AbstractCacheManager

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

public abstract class AbstractCacheManager implements CacheManager, InitializingBean {
    protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractCacheManager.class);
    protected abstract String getCacheInfo();
    protected abstract void loadingCache();
    protected abstract long getSize();
    @Override
    public void afterPropertiesSet() throws Exception { startup(); }
    @Override
    public void initCache() {
        String desc = getCacheName().getDescription();
        LOGGER.info("start init {}", desc);
        loadingCache();
        afterInitCache();
        LOGGER.info("{} end init", desc);
    }
    @Override
    public void refreshCache() {
        String desc = getCacheName().getDescription();
        LOGGER.info("start refresh {}", desc);
        loadingCache();
        afterRefreshCache();
        LOGGER.info("{} end refresh", desc);
    }
    @Override
    public int getOrder() { return getCacheName().getOrder(); }
    @Override
    public void dumpCache() {
        String desc = getCacheName().getDescription();
        LOGGER.info("start print {} {}{}", desc, "
", getCacheInfo());
        LOGGER.info("{} end print", desc);
    }
    @Override
    public long getCacheSize() {
        LOGGER.info("Cache Size Count: {}", getSize());
        return getSize();
    }
    protected void afterInitCache() { }
    protected void afterRefreshCache() { }
    private void startup() {
        try { deployCompletion(); }
        catch (Exception e) {
            LOGGER.error("Cache Component Init Fail:", e);
            throw new RuntimeException("启动加载失败", e);
        }
    }
    private void deployCompletion() {
        // omitted for brevity – registers, sorts, and initializes all managers
    }
}

f. CacheManagerRegistry

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.OrderComparator;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Component
public final class CacheManagerRegistry implements InitializingBean {
    private static final Logger logger = LoggerFactory.getLogger(CacheManagerRegistry.class);
    private static Map<String, CacheManager> managerMap = new ConcurrentHashMap<>();
    public static void register(CacheManager cacheManager) {
        managerMap.put(resolveCacheName(cacheManager.getCacheName().getName()), cacheManager);
    }
    public static void refreshCache(String cacheName) {
        CacheManager cm = managerMap.get(resolveCacheName(cacheName));
        if (cm == null) { logger.warn("cache manager is not exist,cacheName=", cacheName); return; }
        cm.refreshCache();
        cm.dumpCache();
    }
    public static long getCacheSize(String cacheName) {
        CacheManager cm = managerMap.get(resolveCacheName(cacheName));
        if (cm == null) { logger.warn("cache manager is not exist,cacheName=", cacheName); return 0; }
        return cm.getCacheSize();
    }
    public static List<String> getCacheNameList() {
        List<String> list = new ArrayList<>();
        managerMap.forEach((k, v) -> list.add(k));
        return list;
    }
    private static String resolveCacheName(String cacheName) { return cacheName.toUpperCase(); }
    @Override
    public void afterPropertiesSet() throws Exception { startup(); }
    private void startup() { /* omitted – calls deployCompletion */ }
}

3. Usage Steps

1. Add a new enum value to CacheNameEnum for the business cache, e.g.,

SYS_CONFIG("SYS_CONFIG", "System configuration cache", Ordered.LOWEST_PRECEDENCE)

.

2. Create a custom cache manager class extending AbstractCacheManager, for example SysConfigCacheManager.

3. Implement loadingCache() to fetch data from the source and populate the cache (avoid putting all data into a single processor).

4. Provide a method to retrieve data from the cache, because different business scenarios have different query signatures.

a. Example: SysConfigCacheManager

package com.summo.demo.cache.manager;

import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import com.summo.demo.cache.*;
import org.springframework.stereotype.Component;

/** System configuration cache manager */
@Component
public class SysConfigCacheManager extends AbstractCacheManager {
    private static final Lock LOCK = new ReentrantLock();
    private static ConcurrentMap<String, Object> CACHE;
    @Override
    protected String getCacheInfo() { return CacheMessageUtil.toString(CACHE); }
    @Override
    protected void loadingCache() {
        LOCK.lock();
        try {
            CACHE = new ConcurrentHashMap<>();
            CACHE.put("key1", "value1");
            CACHE.put("key2", "value2");
            CACHE.put("key3", "value3");
        } finally { LOCK.unlock(); }
    }
    @Override
    protected long getSize() { return CACHE == null ? 0 : CACHE.size(); }
    @Override
    public CacheNameDomain getCacheName() { return CacheNameEnum.SYS_CONFIG; }
    /** Retrieve a configuration value by key */
    public static Object getConfigByKey(String key) { return CACHE.get(key); }
}

4. Hierarchical Caching with Functional Programming

4.1 Example Scenario

A product‑lookup service first checks Redis; if the entry is missing, it queries the database, stores the result in Redis, and returns the product.
// Pseudo‑code
String goodsInfoStr = redis.get(goodsName);
if (StringUtils.isBlank(goodsInfoStr)) {
    Goods goods = goodsMapper.queryByName(goodsName);
    redis.set(goodsName, JSONObject.toJSONString(goods));
    return goods;
} else {
    return JSON.parseObject(goodsInfoStr, Goods.class);
}

This pattern is not reusable because the query logic is hard‑coded. To make it generic we can pass the cache‑selector and database‑selector as functional parameters.

4.2 Functional‑Programming Template

/**
 * Cache‑first template: try cacheSelector first, fall back to databaseSelector.
 */
public static <T> T selectCacheByTemplate(Supplier<T> cacheSelector, Supplier<T> databaseSelector) {
    try {
        log.info("query data from redis …");
        T t = cacheSelector.get();
        if (t == null) {
            return databaseSelector.get();
        } else {
            return t;
        }
    } catch (Exception e) {
        log.info("query data from database …");
        return databaseSelector.get();
    }
}

The Supplier interface is a functional interface introduced in Java 8.

4.3 Usage Example

@Component
public class UserManager {
    @Autowired
    private CacheService cacheService;
    public Set<String> queryAuthByUserId(Long userId) {
        return BaseUtil.selectCacheByTemplate(
            () -> this.cacheService.queryUserFromRedis(userId),
            () -> this.cacheService.queryUserFromDB(userId)
        );
    }
}

Note: cache consistency must be handled when underlying data changes.

5. Conclusion

Caching can significantly improve API query efficiency, but it is not a silver bullet. Evaluate access frequency and update patterns before caching data, balance the overhead, and continuously tune the strategy to match business needs. A well‑designed cache layer, combined with functional‑programming utilities, leads to clean, reusable, and high‑performance code.
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.

BackendjavaSpring Boot
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

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.