Spring Boot Cache Showdown: When to Choose Redis, Caffeine, or Ehcache
This article compares Redis, Caffeine, and Ehcache for Spring Boot 3.5.0, explaining each cache's architecture, ideal use cases, configuration patterns with code examples, and trade-offs between in-process speed and distributed consistency, plus guidance on multi-level caching with JetCache.
1. Introduction
Environment: Spring Boot 3.5.0. While Redis often comes to mind first for caching, Spring Boot supports three main caching solutions — Redis, Caffeine, and Ehcache — each with distinct architectures and optimal scenarios. Choosing the right one improves performance and reduces system complexity.
2. Cache Characteristics
Caffeine: In-Process, Nanosecond Latency
Caffeine runs inside the JVM, sharing the application's heap memory. No network calls, no serialization, no external dependencies. Cache hits take nanoseconds, comparable to a HashMap lookup. Best for high-read, low-write data that doesn't need cross-instance consistency.
Ehcache: In-Process with Off-Heap and Disk Tiers
Also runs in-process but can overflow to off-heap memory and disk. Older, richer configuration. Core advantage: stores data off-heap to avoid GC pressure when cache volume is large. Suitable when cache size exceeds reasonable heap limits but cross-instance sharing isn't required.
Redis: Distributed, External Service
Runs as a separate service accessed over the network. Every operation involves serialization, network round-trip, and deserialization. Core advantage: all application instances share the same cache data. Required when multiple instances need consistent data, data must survive JVM restarts, atomic operations across concurrent requests are needed, or different services must share cache entries.
3. Practical Examples
3.1 Caffeine for Local High-Frequency Reads
Scenario: Read frequency far exceeds write frequency; no cross-instance consistency needed; raw speed is critical.
Dependency:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>Cache Manager Configuration:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
@Primary
CacheManager userCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return manager;
}
@Bean
CacheManager tieredCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
Map<String, Caffeine<Object, Object>> specs = Map.of(
"countryCodes", Caffeine.newBuilder().maximumSize(300).expireAfterWrite(1, TimeUnit.HOURS),
"userPermissions", Caffeine.newBuilder().maximumSize(5000).expireAfterWrite(5, TimeUnit.MINUTES),
"productPrices", Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(30, TimeUnit.SECONDS)
);
specs.forEach((name, spec) -> manager.registerCustomCache(name, spec.build()));
return manager;
}
}Note: Spring Boot auto-configures Caffeine by default; custom configuration overrides defaults.
Usage:
@Service
public class UserService {
@Cacheable(key = "#id", value = "userCache", cacheManager = "userCacheManager")
public User queryUser(Long id) {
return new User(id, "xxxooo", 33);
}
@Cacheable(key = "#id", value = "userCache", cacheManager = "userCacheManager")
public void removeUser(Long id) { }
}Limitation: Caffeine is bound to the current JVM and cannot share data across instances.
3.2 Ehcache for Large Local Caches
Scenario: Cache entries are large, numerous, or both — storing everything in JVM heap would cause excessive GC pressure. Cross-instance sharing still not required.
Dependencies:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>ehcache.xml Configuration:
<config xmlns='http://www.ehcache.org/v3'>
<persistence directory="e:/app/cache" />
<cache alias="userCache">
<key-type>java.lang.Long</key-type>
<value-type>java.lang.Object</value-type>
<resources>
<heap unit="entries">2000</heap>
<offheap unit="MB">100</offheap>
</resources>
</cache>
<cache-template name="sysDefaults">
<key-type>java.lang.Long</key-type>
<value-type>java.lang.String</value-type>
<heap unit="entries">200</heap>
</cache-template>
<cache alias="bar" uses-template="sysDefaults">
<key-type>java.lang.Number</key-type>
</cache>
<cache alias="simpleCache" uses-template="sysDefaults" />
</config>Cache Manager Bean:
@Bean
JCacheCacheManager cacheManager() throws Exception {
CacheManager cacheManager = Caching.getCachingProvider()
.getCacheManager(getClass().getResource("/ehcache.xml").toURI(), getClass().getClassLoader());
return new JCacheCacheManager(cacheManager);
}Summary: Ehcache's off-heap and disk tiering adds complexity most applications don't need. If cache fits comfortably in heap (true for most services), Caffeine's simpler API and better performance make it the preferred in-process choice. Only choose Ehcache when heap pressure is a measured, real problem — not a theoretical concern.
3.3 Redis for Distributed Cross-Instance Sharing
Scenario: Multiple app instances must access the same cache data; data must persist across JVM restarts; atomic operations across concurrent requests are required; cache entries shared across completely different services.
Dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>application.yml:
spring:
cache:
type: redis
redis:
time-to-live: 600000
cache-null-values: false
enable-statistics: trueCustom RedisCacheManager:
@Bean
RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> configs = Map.of(
"userCache", defaults.entryTtl(Duration.ofMinutes(60)),
"productCache", defaults.entryTtl(Duration.ofHours(24))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(defaults)
.withInitialCacheConfigurations(configs)
.build();
}Usage:
@Cacheable(key = "#id", value = "userCache", cacheManager = "redisCacheManager")
public User queryUser(Long id) {
return new User(id, "xxxooo", 33);
}Summary: Redis adds operational complexity — cluster setup, monitoring, scaling — and every cache operation incurs network latency. This introduces an external failure point requiring circuit breakers and fallback strategies. These costs are justified only when cross-instance consistency is genuinely needed; for reference data that Caffeine handles perfectly, they're unnecessary overhead.
3.4 Multi-Level Caching
Typical pattern: Caffeine as L1 (front) cache, Redis as L2 (back) cache, ensuring cross-instance consistency. On cache miss, check Caffeine first, then Redis, then database. The article recommends the JetCache framework for this purpose, with a link to a dedicated article: "Abandon Spring Cache! Choose Multi-Level Cache Framework JetCache".
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
