
Spring Cache
- 35 installs
- 27 repo stars
- Updated July 17, 2026
- claude-dev-suite/claude-dev-suite
Reference for the Spring Cache abstraction covering @Cacheable, @CacheEvict, @CachePut, cache managers (Caffeine, Redis, EhCache), TTL, and cache keys.
About
Documents the Spring Cache abstraction including caching annotations, cache managers like Caffeine and Redis, TTL configuration, and conditional caching. A developer uses it to add declarative caching to a Spring Boot app.
- @Cacheable, @CacheEvict, and @CachePut annotations
- Cache managers (Caffeine, Redis, EhCache) and TTL
Spring Cache by the numbers
- 35 all-time installs (skills.sh)
- Ranked #48 of 89 Java & JVM skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill spring-cacheAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 35 |
|---|---|
| repo stars | ★ 27 |
| Last updated | July 17, 2026 |
| Repository | claude-dev-suite/claude-dev-suite ↗ |
What it does
Reference for the Spring Cache abstraction covering @Cacheable, @CacheEvict, @CachePut, cache managers (Caffeine, Redis, EhCache), TTL, and cache keys.
Files
Spring Cache
Quick Start
@SpringBootApplication
@EnableCaching
public class Application {}
@Service
public class UserService {
@Cacheable("users")
public User findById(Long id) {
return userRepository.findById(id).orElseThrow();
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m---
Cache Annotations
@Cacheable
@Cacheable("products")
public Product findById(Long id) { }
@Cacheable(value = "products", key = "#category + '-' + #status")
public List<Product> findByCategoryAndStatus(String category, String status) { }
@Cacheable(value = "products", condition = "#id > 0")
public Product findByIdConditional(Long id) { }
@Cacheable(value = "products", unless = "#result == null")
public Product findByIdUnlessNull(Long id) { }
@Cacheable(value = "products", sync = true) // One thread populates
public Product findByIdSync(Long id) { }@CacheEvict
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) { }
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() { }
@CacheEvict(value = "products", key = "#id", beforeInvocation = true)
public void deleteProductBeforeInvocation(Long id) { }@CachePut
@CachePut(value = "products", key = "#product.id")
public Product saveProduct(Product product) {
return productRepository.save(product);
}
@CachePut(value = "products", key = "#result.id")
public Product createProduct(CreateProductRequest request) {
return productRepository.save(new Product(request));
}@Caching (Multiple Operations)
@Caching(
put = {
@CachePut(value = "products", key = "#result.id"),
@CachePut(value = "productsBySku", key = "#result.sku")
},
evict = {
@CacheEvict(value = "productList", allEntries = true)
}
)
public Product createProduct(CreateProductRequest request) { }@CacheConfig (Class-Level)
@Service
@CacheConfig(cacheNames = "products", keyGenerator = "customKeyGenerator")
public class ProductService {
@Cacheable // Uses class config
public Product findById(Long id) { }
@Cacheable(cacheNames = "inventory") // Override cache name
public Inventory getInventory(Long productId) { }
}Full Reference: See managers.md for Caffeine, Redis, EhCache configurations.
---
Quick Cache Manager Setup
Caffeine (Single Instance)
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return manager;
}Redis (Distributed)
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}Full Reference: See advanced.md for Multi-Level Caching, Metrics, Synchronization.
---
Best Practices
| Do | Don't |
|---|---|
| Use Caffeine for single-instance | Skip TTL configuration |
| Use Redis for distributed | Cache mutable objects |
| Configure TTL always | Ignore cache eviction |
| Use sync=true for expensive ops | Use high cardinality keys |
| Implement cache metrics | Cache sensitive data unencrypted |
---
Production Checklist
- [ ] Cache provider configured (Caffeine/Redis)
- [ ] TTL configured for every cache
- [ ] Cache eviction on write operations
- [ ] Metrics configured
- [ ] Serialization tested
- [ ] Distributed lock for critical ops
---
When NOT to Use This Skill
- Distributed caching - Use
spring-data-redis - Redis operations - Use
redisskill - Session storage - Use Spring Session
---
Common Pitfalls
| Error | Cause | Solution |
|---|---|---|
| Cache not working | Internal call (same bean) | Use self-injection |
| Null pointer | Null values cached | Use unless = "#result == null" |
| Memory leak | TTL not configured | Set expireAfterWrite |
| Serialization error | Non-serializable objects | Implement Serializable |
---
Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Caching mutable objects | Stale data | Cache immutable data |
| No TTL configured | Stale cache forever | Set expireAfterWrite |
| @Cacheable on void | No effect | Only cache with return |
| No cache sync | Race conditions | Use sync=true or locks |
---
Quick Troubleshooting
| Problem | Diagnostic | Fix |
|---|---|---|
| Cache not working | Check @EnableCaching | Add annotation |
| Wrong data cached | Check cache key | Define explicit key |
| Cache not evicted | Check key expression | Verify key matches |
| Self-invocation bypass | Same class call | Inject self |
---
Reference Files
| File | Content |
|---|---|
| managers.md | Caffeine, Redis, EhCache, Key Generators |
| advanced.md | Multi-Level, Metrics, Sync, Testing |
---
External Documentation
Advanced Cache Patterns
Multi-Level Caching
@Configuration
@EnableCaching
public class MultiLevelCacheConfig {
@Bean
@Primary
public CacheManager cacheManager(
CaffeineCacheManager localCacheManager,
RedisCacheManager redisCacheManager) {
return new CompositeCacheManager(localCacheManager, redisCacheManager);
}
@Bean
public CaffeineCacheManager localCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(5)));
return manager;
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)))
.build();
}
}
// Custom CompositeCacheManager
public class CompositeCacheManager implements CacheManager {
private final List<CacheManager> cacheManagers;
@Override
public Cache getCache(String name) {
return new CompositeCache(name,
cacheManagers.stream()
.map(cm -> cm.getCache(name))
.filter(Objects::nonNull)
.toList()
);
}
@Override
public Collection<String> getCacheNames() {
return cacheManagers.stream()
.flatMap(cm -> cm.getCacheNames().stream())
.distinct()
.toList();
}
}
// CompositeCache - Read-through L1 -> L2, Write-through L1 + L2
public class CompositeCache implements Cache {
private final String name;
private final List<Cache> caches;
@Override
public ValueWrapper get(Object key) {
for (Cache cache : caches) {
ValueWrapper value = cache.get(key);
if (value != null) {
// Populate higher-level caches
populateHigherLevelCaches(key, value.get(), cache);
return value;
}
}
return null;
}
@Override
public void put(Object key, Object value) {
// Write to all levels
caches.forEach(cache -> cache.put(key, value));
}
@Override
public void evict(Object key) {
caches.forEach(cache -> cache.evict(key));
}
private void populateHigherLevelCaches(Object key, Object value, Cache sourceCache) {
for (Cache cache : caches) {
if (cache == sourceCache) break;
cache.put(key, value);
}
}
}---
Cache Metrics
@Configuration
public class CacheMetricsConfig {
@Bean
public CacheManager cacheManagerWithMetrics(MeterRegistry meterRegistry) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
// Register metrics for each cache
cacheManager.setCacheNames(List.of("users", "products", "sessions"));
return cacheManager;
}
// Manual metrics
@Component
public class CacheMetricsCollector {
private final CacheManager cacheManager;
private final MeterRegistry meterRegistry;
@Scheduled(fixedRate = 60000)
public void collectMetrics() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache instanceof CaffeineCache caffeineCache) {
com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache =
caffeineCache.getNativeCache();
CacheStats stats = nativeCache.stats();
Gauge.builder("cache.size", nativeCache, c -> c.estimatedSize())
.tag("cache", cacheName)
.register(meterRegistry);
Gauge.builder("cache.hit.rate", stats, CacheStats::hitRate)
.tag("cache", cacheName)
.register(meterRegistry);
Gauge.builder("cache.eviction.count", stats, CacheStats::evictionCount)
.tag("cache", cacheName)
.register(meterRegistry);
}
});
}
}
}---
Cache Synchronization
// To avoid race conditions during cache population
@Service
public class ProductService {
// sync=true ensures that only one thread populates the cache
@Cacheable(value = "products", key = "#id", sync = true)
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}
}
// Distributed lock for Redis
@Component
public class DistributedCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final RedisLockRegistry lockRegistry;
@Cacheable(value = "expensive-data", key = "#key")
public Object getExpensiveData(String key) {
Lock lock = lockRegistry.obtain("cache-lock:" + key);
try {
if (lock.tryLock(10, TimeUnit.SECONDS)) {
try {
// Check cache again after acquiring the lock
Object cached = redisTemplate.opsForValue().get("expensive-data:" + key);
if (cached != null) {
return cached;
}
// Compute expensive operation
Object result = computeExpensiveOperation(key);
// Store in cache
redisTemplate.opsForValue().set(
"expensive-data:" + key,
result,
Duration.ofMinutes(10)
);
return result;
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
throw new RuntimeException("Could not acquire lock");
}
}---
Testing Cache
@SpringBootTest
class CacheTest {
@Autowired
private ProductService productService;
@Autowired
private CacheManager cacheManager;
@BeforeEach
void clearCache() {
cacheManager.getCacheNames()
.forEach(name -> cacheManager.getCache(name).clear());
}
@Test
void findById_shouldCacheResult() {
Long productId = 1L;
// First call - cache miss
Product first = productService.findById(productId);
// Second call - cache hit
Product second = productService.findById(productId);
assertThat(first).isSameAs(second);
// Verify cache contains the value
Cache cache = cacheManager.getCache("products");
assertThat(cache.get(productId)).isNotNull();
}
@Test
void updateProduct_shouldEvictCache() {
Long productId = 1L;
// Populate cache
productService.findById(productId);
// Verify cache populated
Cache cache = cacheManager.getCache("products");
assertThat(cache.get(productId)).isNotNull();
// Update - should evict
productService.updateProduct(productId, new ProductUpdate());
// Verify cache evicted
assertThat(cache.get(productId)).isNull();
}
}
// Test with mock CacheManager
@SpringBootTest
@AutoConfigureCache
class CacheDisabledTest {
@MockBean
private CacheManager cacheManager;
@Test
void test_withNoOpCache() {
when(cacheManager.getCache(anyString()))
.thenReturn(new NoOpCache("test"));
// Test logic without real caching
}
}Cache Managers
Caffeine (Consigliato per single instance)
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats()); // Per metriche
return cacheManager;
}
// Cache multiple con configurazioni diverse
@Bean
public CacheManager multiCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(List.of(
buildCache("users", 1000, Duration.ofMinutes(30)),
buildCache("products", 5000, Duration.ofMinutes(10)),
buildCache("sessions", 10000, Duration.ofMinutes(5)),
buildCache("config", 100, Duration.ofHours(1))
));
return cacheManager;
}
private CaffeineCache buildCache(String name, int maxSize, Duration ttl) {
return new CaffeineCache(name, Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(ttl)
.recordStats()
.build());
}
}# Via properties
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=600s,recordStats
cache-names:
- users
- products
- sessions---
Redis (Per distributed caching)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
// Configurazioni per cache specifiche
Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"users", defaultConfig.entryTtl(Duration.ofMinutes(30)),
"products", defaultConfig.entryTtl(Duration.ofMinutes(10)),
"sessions", defaultConfig.entryTtl(Duration.ofMinutes(5)),
"config", defaultConfig.entryTtl(Duration.ofHours(1))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
// Con prefix personalizzato
@Bean
public CacheManager prefixedCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.prefixCacheNameWith("myapp:")
.entryTtl(Duration.ofMinutes(10));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}# application.yml
spring:
data:
redis:
host: localhost
port: 6379
password: ${REDIS_PASSWORD:}
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
cache:
type: redis
redis:
time-to-live: 600000 # 10 minuti in ms
cache-null-values: false
key-prefix: "cache:"
use-key-prefix: true---
EhCache 3
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency><!-- ehcache.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
<cache alias="users">
<key-type>java.lang.Long</key-type>
<value-type>com.example.User</value-type>
<expiry>
<ttl unit="minutes">30</ttl>
</expiry>
<resources>
<heap unit="entries">1000</heap>
<offheap unit="MB">100</offheap>
</resources>
</cache>
<cache alias="products">
<key-type>java.lang.Long</key-type>
<value-type>com.example.Product</value-type>
<expiry>
<ttl unit="minutes">10</ttl>
</expiry>
<resources>
<heap unit="entries">5000</heap>
</resources>
</cache>
</config>spring:
cache:
jcache:
config: classpath:ehcache.xml---
Custom Key Generator
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName())
.append(".")
.append(method.getName());
for (Object param : params) {
key.append("_");
if (param != null) {
if (param instanceof Pageable pageable) {
key.append("p").append(pageable.getPageNumber())
.append("s").append(pageable.getPageSize());
} else {
key.append(param.hashCode());
}
} else {
key.append("null");
}
}
return key.toString();
}
}
// Key generator per filtri complessi
@Component("filterKeyGenerator")
public class FilterKeyGenerator implements KeyGenerator {
private final ObjectMapper objectMapper;
@Override
public Object generate(Object target, Method method, Object... params) {
try {
String className = target.getClass().getSimpleName();
String methodName = method.getName();
String paramsHash = DigestUtils.md5DigestAsHex(
objectMapper.writeValueAsBytes(params)
);
return className + ":" + methodName + ":" + paramsHash;
} catch (JsonProcessingException e) {
throw new RuntimeException("Error generating cache key", e);
}
}
}---
Cache Resolver
// Cache resolver dinamico basato su runtime conditions
@Component("dynamicCacheResolver")
public class DynamicCacheResolver implements CacheResolver {
private final CacheManager localCacheManager;
private final CacheManager redisCacheManager;
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
String cacheName = getCacheName(context);
// Usa Redis per dati condivisi, locale per altri
if (isSharedData(context)) {
return List.of(redisCacheManager.getCache(cacheName));
} else {
return List.of(localCacheManager.getCache(cacheName));
}
}
private boolean isSharedData(CacheOperationInvocationContext<?> context) {
return context.getTarget().getClass().isAnnotationPresent(SharedCache.class);
}
private String getCacheName(CacheOperationInvocationContext<?> context) {
return context.getOperation().getCacheNames().iterator().next();
}
}
// Uso
@Service
@SharedCache
public class SharedDataService {
@Cacheable(cacheResolver = "dynamicCacheResolver")
public Data getSharedData(String key) {
return repository.findByKey(key);
}
}