Spring: Cache Abstraction (@Cacheable, @CacheEvict)
Spring's cache abstraction lets you add caching to your application through annotations without coupling your code to a specific cache implementation. You annotate methods with @Cacheable, @CachePut, or @CacheEvict and Spring transparently handles storing and retrieving results from the configured cache backend.
Contents
Enabling Caching with @EnableCaching
Add @EnableCaching@Configuration@SpringBootApplication
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig {
// Declare a CacheManager bean here, or rely on Spring Boot auto-configuration
}
ConcurrentMapCacheManagerCacheManager@Cacheable
@Cacheable
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
private final ProductRepository productRepo;
public ProductService(ProductRepository productRepo) {
this.productRepo = productRepo;
}
// Cache name: "products", default key: method parameter (productId)
@Cacheable("products")
public Product findById(Long productId) {
System.out.println("Cache miss — fetching from DB for id: " + productId);
return productRepo.findById(productId).orElseThrow();
}
// Custom key using SpEL
@Cacheable(value = "products", key = "#sku.toUpperCase()")
public Product findBySku(String sku) {
return productRepo.findBySku(sku.toUpperCase());
}
// Conditional caching — only cache active products
@Cacheable(value = "products", condition = "#result != null && #result.active")
public Product findActiveProduct(Long productId) {
return productRepo.findById(productId).orElse(null);
}
}
@CachePut
@CachePut
// Always executes AND updates the cache — used for update operations
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
Product saved = productRepo.save(product);
return saved; // return value is stored in cache
}
@Cacheable@CachePut@Cacheable@CacheEvict
@CacheEvict
// Remove a single entry by key
@CacheEvict(value = "products", key = "#productId")
public void deleteProduct(Long productId) {
productRepo.deleteById(productId);
}
// Remove all entries from the cache
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() {
// called to force full cache refresh
}
// Evict BEFORE the method runs (useful when the method may fail)
@CacheEvict(value = "products", key = "#productId", beforeInvocation = true)
public void deleteProductEarly(Long productId) {
productRepo.deleteById(productId);
}
Cache Key Generation
The default key is generated from all method parameters. Use the key
// Composite key from multiple parameters
@Cacheable(value = "products", key = "#category + ':' + #page")
public List<Product> findByCategory(String category, int page) {
return productRepo.findByCategory(category, page);
}
// Key from a field of a parameter object
@Cacheable(value = "orders", key = "#request.orderId")
public Order getOrder(OrderRequest request) {
return orderRepo.findById(request.getOrderId()).orElseThrow();
}
// Custom KeyGenerator bean
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) ->
target.getClass().getSimpleName() + ":" + method.getName() + ":" +
Arrays.stream(params).map(Object::toString).collect(Collectors.joining(","));
}
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public List<Product> searchProducts(String query, String category) {
return productRepo.search(query, category);
}
ConcurrentMapCacheManager vs Caffeine vs Redis
Each cache manager suits a different need: an in-memory map for tests, Caffeine for a single instance, Redis when instances share a cache.
// --- ConcurrentMapCacheManager (default, in-memory, no eviction) ---
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("products", "orders");
}
// --- Caffeine (in-memory with eviction, TTL, size limits) ---
// dependency: spring-boot-starter-cache + caffeine
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return manager;
}
// application.properties alternative for Caffeine:
// spring.cache.type=caffeine
// spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=10m
// --- Redis (distributed cache) ---
// dependency: spring-boot-starter-data-redis + spring-boot-starter-cache
// application.properties:
// spring.data.redis.host=localhost
// spring.data.redis.port=6379
// spring.cache.type=redis
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
}
| Cache Manager | Eviction | Distribution | Use case |
|---|---|---|---|
| ConcurrentMapCacheManager | None | Single node | Development / tests |
| CaffeineCacheManager | TTL, LRU, size | Single node | Production single-instance apps |
| RedisCacheManager | TTL | Multi-node | Clustered / cloud deployments |
@Caching for Multiple Operations
Use @Caching
import org.springframework.cache.annotation.Caching;
// Update the product, update "products" cache by id, and evict the category cache
@Caching(
put = { @CachePut(value = "products", key = "#product.id") },
evict = { @CacheEvict(value = "product-by-category", key = "#product.category") }
)
public Product updateProduct(Product product) {
return productRepo.save(product);
}
// Multiple @Cacheable with different cache names
@Caching(cacheable = {
@Cacheable(value = "products", key = "#id"),
@Cacheable(value = "featured-products", key = "#id", condition = "#result?.featured")
})
public Product findProduct(Long id) {
return productRepo.findById(id).orElseThrow();
}