Backend Development 16 min read

Understanding Spring Boot Caching with JCache, Annotations, and Redis Integration

This article explains Spring Boot's caching mechanism based on the JSR‑107 JCache specification, details core interfaces and implementations like Cache, AbstractValueAdaptingCache, and ConcurrentMapCache, demonstrates cache annotations such as @Cacheable, @CachePut and @CacheEvict, and shows how to replace the default in‑memory cache with Redis using Docker, StringRedisTemplate, and custom serialization.

Top Architect
Top Architect
Top Architect
Understanding Spring Boot Caching with JCache, Annotations, and Redis Integration

Spring Boot caching relies on the JSR‑107 (JCache) specification, which defines core interfaces such as CachingProvider , CacheManager , Cache , Entry and Expiry .

The Cache interface abstracts operations like get , put and evict , and Spring provides implementations such as ConcurrentMapCache and AbstractValueAdaptingCache that handle null values and optional serialization.

Spring’s cache abstraction is enabled with @EnableCaching and uses annotations @Cacheable , @CachePut and @CacheEvict to declaratively manage method results, supporting key generation, conditions, SpEL expressions and sync options.

For production use, Redis can replace the default in‑memory cache. The article shows how to start a Redis container with Docker, configure StringRedisTemplate and RedisTemplate , and customize serialization to store objects as JSON instead of Java serialization.

Custom CacheManager and RedisCacheManager examples illustrate how to control cache prefixes and serialization strategies, allowing Spring to manage Redis‑backed caches alongside other cache implementations.

public interface Cache {
    String getName();
    Object getNativeCache();
    @Nullable
    Cache.ValueWrapper get(Object key);
    @Nullable
T get(Object key, @Nullable Class
type);
    @Nullable
T get(Object key, Callable
valueLoader);
    void put(Object key, @Nullable Object value);
    @Nullable
    ValueWrapper putIfAbsent(Object key, @Nullable Object value);
    void evict(Object key);
    boolean evictIfPresent(Object key);
    void clear();
    boolean invalidate();
    interface ValueWrapper {
        @Nullable Object get();
    }
}

public abstract class AbstractValueAdaptingCache implements Cache {
    private final boolean allowNullValues;
    protected AbstractValueAdaptingCache(boolean allowNullValues) { this.allowNullValues = allowNullValues; }
    public final boolean isAllowNullValues() { return this.allowNullValues; }
    @Nullable
    public ValueWrapper get(Object key) { return toValueWrapper(lookup(key)); }
    @Nullable
    protected abstract Object lookup(Object key);
    @Nullable
    protected Object fromStoreValue(@Nullable Object storeValue) { return this.allowNullValues && storeValue == NullValue.INSTANCE ? null : storeValue; }
    @Nullable
    protected Object toStoreValue(@Nullable Object userValue) { if (userValue == null) { if (this.allowNullValues) return NullValue.INSTANCE; else throw new IllegalArgumentException("Null values not allowed"); } return userValue; }
    @Nullable
    protected ValueWrapper toValueWrapper(@Nullable Object storeValue) { return storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null; }
}

public class ConcurrentMapCache extends AbstractValueAdaptingCache {
    private final String name;
    private final ConcurrentMap
store;
    private final SerializationDelegate serialization;
    public ConcurrentMapCache(String name) { this(name, new ConcurrentHashMap(256), true); }
    public ConcurrentMapCache(String name, boolean allowNullValues) { this(name, new ConcurrentHashMap(256), allowNullValues); }
    protected ConcurrentMapCache(String name, ConcurrentMap
store, boolean allowNullValues, @Nullable SerializationDelegate serialization) {
        super(allowNullValues);
        Assert.notNull(name, "Name must not be null");
        Assert.notNull(store, "Store must not be null");
        this.name = name;
        this.store = store;
        this.serialization = serialization;
    }
    public String getName() { return this.name; }
    public ConcurrentMap
getNativeCache() { return this.store; }
    @Nullable
    protected Object lookup(Object key) { return this.store.get(key); }
    public void put(Object key, @Nullable Object value) { this.store.put(key, toStoreValue(value)); }
    public void evict(Object key) { this.store.remove(key); }
    public boolean evictIfPresent(Object key) { return this.store.remove(key) != null; }
    public void clear() { this.store.clear(); }
    public boolean invalidate() { boolean notEmpty = !this.store.isEmpty(); this.store.clear(); return notEmpty; }
}
BackendJavaCacheRedisSpring BootJCache
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.