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 to a @Configuration class (or @SpringBootApplication). This activates the annotation-driven cache infrastructure.

Java
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
}
Spring Boot auto-configures a ConcurrentMapCacheManager when no other cache provider is on the classpath. To use Caffeine or Redis, add the relevant dependency and optionally configure a CacheManager bean.

@Cacheable

@Cacheable causes Spring to check the cache before executing the method. If a value is found for the computed key, the method body is skipped and the cached result is returned. If not found, the method executes and the result is stored in the cache.

Java
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);
    }
}
Caching is implemented via a proxy: self-invocation (calling a cached method from within the same class) bypasses the cache. Move cached methods to a separate bean if needed.

@CachePut

@CachePut always executes the method and updates the cache with the result. Use it for write/update operations where you want to keep the cache fresh.

Java
// 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
}
Unlike @Cacheable, @CachePut never skips method execution. It is meant to update the cached value after a write, so subsequent @Cacheable calls return the fresh data.

@CacheEvict

@CacheEvict removes one or all entries from a cache. Use it on delete or invalidation operations.

Java
// 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 attribute with SpEL expressions for custom keys.

Java
// 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.

Java
// --- 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
Java
// --- 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 ManagerEvictionDistributionUse case
ConcurrentMapCacheManagerNoneSingle nodeDevelopment / tests
CaffeineCacheManagerTTL, LRU, sizeSingle nodeProduction single-instance apps
RedisCacheManagerTTLMulti-nodeClustered / cloud deployments

@Caching for Multiple Operations

Use @Caching to combine multiple cache annotations on a single method.

Java
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();
}