Use Spring Cache Annotations with Redis Backend
Apply Spring's @Cacheable, @CachePut, and @CacheEvict annotations with a Redis cache manager for declarative caching in Java applications.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Spring Framework provides declarative caching through annotations: @Cacheable, @CachePut, @CacheEvict, and @Caching. These annotations intercept method calls, check the cache before execution, and store results after execution — all without modifying business logic. With a Redis backend, the cache is shared across all application instances. Below: configuring Spring Cache with Redis, using each annotation, conditional caching, multi-cache operations, and TTL management.
When to Use This
- Spring Boot applications that need caching without coupling business logic to cache code
- Service-layer methods with expensive database queries or computations
- Multi-instance deployments that need a shared cache (Redis backend)
- Any Spring project where declarative caching reduces boilerplate
Prerequisites
- Java 17+
- Spring Boot 3+
- Redis server
Solution
1. Add Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. Configure Redis Cache Manager
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
// Per-cache TTL configuration
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
cacheConfigs.put("users", config.entryTtl(Duration.ofMinutes(5)));
cacheConfigs.put("products", config.entryTtl(Duration.ofMinutes(30)));
cacheConfigs.put("config", config.entryTtl(Duration.ofHours(24)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
}
3. @Cacheable — Skip Method on Cache Hit
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(String id) {
// Only executed on cache miss
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
// Cache with composite key
@Cacheable(value = "users", key = "#email")
public User getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
// Cache with conditional — only cache if result is active
@Cacheable(value = "users", key = "#id", condition = "#result.active == true")
public User getUserByIdConditional(String id) {
return userRepository.findById(id).orElseThrow();
}
// Cache unless result is null
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findUserById(String id) {
return userRepository.findById(id).orElse(null);
}
}
4. @CachePut — Always Execute, Update Cache
@Service
public class UserService {
// Method always executes, result replaces cache entry
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
// Update cache after creating
@CachePut(value = "users", key = "#result.id")
public User createUser(UserDto dto) {
User user = new User(dto.getEmail(), dto.getName());
return userRepository.save(user);
}
}
5. @CacheEvict — Remove from Cache
@Service
public class UserService {
// Evict single entry
@CacheEvict(value = "users", key = "#id")
public void deleteUser(String id) {
userRepository.deleteById(id);
}
// Evict all entries in cache
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
// No-op — just clears the cache
}
// Evict before method execution (e.g., before a bulk update)
@CacheEvict(value = "users", allEntries = true, beforeInvocation = true)
public void bulkUpdateUsers(List<User> users) {
userRepository.saveAll(users);
}
// Conditional eviction
@CacheEvict(value = "users", key = "#user.id", condition = "#user.active == false")
public void deactivateUser(User user) {
user.setActive(false);
userRepository.save(user);
}
}
6. @Caching — Multiple Cache Operations
@Service
public class UserService {
@Caching(
evict = {
@CacheEvict(value = "users", key = "#id"),
@CacheEvict(value = "users", key = "#user.email"),
@CacheEvict(value = "userList", allEntries = true),
}
)
public User updateUserAndEvictCaches(String id, UserDto dto) {
User user = userRepository.findById(id).orElseThrow();
user.setEmail(dto.getEmail());
user.setName(dto.getName());
return userRepository.save(user);
}
}
7. Custom Key Generator
@Configuration
public class CacheConfig {
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
sb.append(param.hashCode()).append(":");
}
return sb.toString();
};
}
}
Usage:
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User getUser(String id, String tenantId) {
return userRepository.findByIdAndTenant(id, tenantId);
}
8. Multi-Cache @CacheConfig
@Service
@CacheConfig(cacheNames = {"users", "userList"}) // Default cache names for the class
public class UserService {
@Cacheable(key = "#id")
public User getUserById(String id) {
return userRepository.findById(id).orElseThrow();
}
@CacheEvict(allEntries = true)
public void clearAll() {}
}
How It Works
- AOP proxy: Spring wraps
@Cacheablebeans in a proxy. When a method is called, the proxy intercepts, checks the cache, and either returns the cached value or executes the method and stores the result. @Cacheable: Before method execution, the proxy checks the cache for the key. On hit, the cached value is returned and the method is skipped. On miss, the method executes and the result is cached.@CachePut: The method always executes. After execution, the result is placed in the cache, overwriting any existing entry. Use for updates where you want fresh data in both DB and cache.@CacheEvict: Removes entries from the cache.allEntries = trueclears the entire cache.beforeInvocation = trueevicts before the method runs (useful for rollback safety).- Key resolution: By default, Spring uses all method parameters as the key. Use
key = "#id"to specify a single parameter, orkeyGeneratorfor custom logic.
Variants
Caffeine + Redis Multi-Level Cache
@Configuration
public class CacheConfig {
@Bean
@Primary
public CompositeCacheManager cacheManager(
RedisCacheManager redisManager,
CaffeineCacheManager caffeineManager) {
CompositeCacheManager manager = new CompositeCacheManager(
caffeineManager, // L1 — checked first
redisManager // L2 — checked second
);
manager.setFallbackToNoOpCache(false);
return manager;
}
}
Cache with TTL per Entry (Redis TTL)
@Cacheable(value = "users", key = "#id")
@CacheEvict(value = "users", key = "#id", condition = "#result.updatedAt > T(java.time.Instant).now().minusSeconds(300)")
public User getUser(String id) {
return userRepository.findById(id).orElseThrow();
}
Async Cache (Spring 6+)
@Cacheable(value = "users", key = "#id")
public CompletableFuture<User> getUserAsync(String id) {
return CompletableFuture.supplyAsync(() ->
userRepository.findById(id).orElseThrow()
);
}
Programmatic Cache Access
@Service
public class UserService {
@Autowired
private CacheManager cacheManager;
public void manualCacheOperation(String userId) {
Cache cache = cacheManager.getCache("users");
if (cache != null) {
// Manual put
cache.put(userId, new User(userId, "Alice"));
// Manual get
User cached = cache.get(userId, User.class);
// Manual evict
cache.evict(userId);
}
}
}
Best Practices
-
For a deeper guide, see Implement Redis Cache Invalidation in Node.js.
-
Use
@Cacheablefor reads,@CachePutfor writes,@CacheEvictfor deletes: Each annotation has a specific purpose. Don’t use@Cacheableon mutating methods. -
Set per-cache TTLs: Different data has different freshness requirements. Configure TTLs in the
RedisCacheManager, not globally. -
Use
unlessto skip caching nulls or unwanted results:unless = "#result == null"prevents caching null values that would mask real misses. -
Evict on writes: After
updateordelete, evict the cache entry. Otherwise, stale data is served until TTL expires. -
Use
allEntries = truesparingly: Clearing the entire cache causes a spike in cache misses. Evict specific keys when possible. -
Disable
nullcaching: UsedisableCachingNullValues()to prevent cachingnullresults, which can mask database errors.
Common Mistakes
- Using
@Cacheableon update methods:@Cacheableskips the method on cache hit — updates never execute. Use@CachePutfor writes. - Missing
@EnableCaching: Without this annotation on a@Configurationclass, cache annotations are no-ops. - Key collisions between methods: Two methods with
@Cacheable("users")and different parameters can collide if keys overlap. Use distinct key expressions. - Caching mutable objects: If the cached object is modified after caching, the cache holds a reference to the mutated object. Use immutable DTOs or deep copies.
- Not handling cache failures: If Redis is down, cache operations throw exceptions. Configure a fallback or use
errorHandlerto log and continue.
FAQ
Spring Cache vs manual caching — which is better?
Spring Cache annotations are declarative and reduce boilerplate. They work well for simple cache-aside patterns. For complex logic (conditional refresh, multi-level, stampede prevention), use programmatic caching with CacheManager or a dedicated cache library.
Can I use @Cacheable with reactive Spring (WebFlux)?
Spring 6+ supports @Cacheable with reactive types (Mono, Flux). The cache stores the reactive wrapper, not the resolved value. For proper reactive caching, use Mono.cache() or a reactive cache library.
How do I set different TTLs for different caches?
Configure per-cache TTLs in RedisCacheManager using withInitialCacheConfigurations(Map<String, RedisCacheConfiguration>). Each cache name gets its own RedisCacheConfiguration with a specific TTL.
What happens when Redis is unavailable?
By default, cache operations throw exceptions. Configure a CacheErrorHandler to log errors and fall back to method execution:
@Bean
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override public void handleCacheGetError(RuntimeException e, Cache cache, Object key) { log.warn("Cache get failed", e); }
@Override public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) { log.warn("Cache put failed", e); }
@Override public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { log.warn("Cache evict failed", e); }
@Override public void handleCacheClearError(RuntimeException e, Cache cache) { log.warn("Cache clear failed", e); }
};
}
Can I use @Cacheable with conditional TTL?
Spring’s @Cacheable doesn’t support per-entry TTL. Use @CachePut with a custom Redis template that sets TTL based on the value, or use programmatic caching for this use case.
Is this solution production-ready?
Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.
What are the performance characteristics?
Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.
How do I debug issues with this approach?
Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language’s debugger to step through edge cases.
Related Resources
Configure Caffeine Cache in Java with Eviction Policies
Set up Caffeine cache in a Java application with size-based, time-based, and weighted eviction policies for high-performance local caching.
RecipeImplement Redis Cache Invalidation in Node.js
Invalidate Redis cache entries in Node.js with TTL expiry, explicit deletion, pattern-based clearing, and pub/sub-based distributed invalidation.
GuideComplete Guide to API Versioning Strategies
Version REST and GraphQL APIs with URI, header, query param, and content negotiation strategies. Covers deprecation, sunset, and migration patterns.
RecipeCache HTTP Responses with httpx and CacheControl in Python
Cache HTTP responses in Python using httpx with CacheControl for HTTP-compliant caching, ETag handling, and conditional requests.