Redis Caching with Spring Boot: Complete Implementation Guide
Master Redis caching in Spring Boot applications. Learn cache configuration, annotations, TTL management, and performance optimization techniques.
Moshiour Rahman
Advertisement
Why Caching?
Caching dramatically improves application performance by storing frequently accessed data in memory. Redis is an in-memory data store perfect for caching.
Benefits
| Without Cache | With Cache |
|---|---|
| Database hit every request | Memory-fast responses |
| Higher latency | Sub-millisecond access |
| More server load | Reduced database load |
| Higher costs | Better scalability |
Setup
Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
Docker Compose for Redis
version: '3.8'
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
volumes:
redis_data:
Configuration
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
password: # optional
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
cache:
type: redis
redis:
time-to-live: 3600000 # 1 hour in milliseconds
cache-null-values: false
Basic Configuration
Enable Caching
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
)
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
Multiple Cache Configurations
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// Default configuration
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
);
// Custom configurations for specific caches
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put("users", defaultConfig.entryTtl(Duration.ofHours(1)));
cacheConfigurations.put("products", defaultConfig.entryTtl(Duration.ofMinutes(15)));
cacheConfigurations.put("sessions", defaultConfig.entryTtl(Duration.ofMinutes(30)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}
Cache Annotations
@Cacheable
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// Cache the result with user ID as key
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
log.info("Fetching user from database: {}", id);
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}
// Cache with custom key
@Cacheable(value = "users", key = "#email")
public User getUserByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
// Cache with SpEL expression
@Cacheable(value = "users", key = "'user_' + #id + '_' + #includeDetails")
public UserDTO getUserDetails(Long id, boolean includeDetails) {
// Implementation
}
// Conditional caching
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User getUserConditional(Long id) {
return userRepository.findById(id).orElse(null);
}
// Cache unless result is null
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserUnlessNull(Long id) {
return userRepository.findById(id).orElse(null);
}
}
@CachePut
@Service
public class UserService {
// Always execute method and update cache
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
log.info("Updating user: {}", user.getId());
return userRepository.save(user);
}
@CachePut(value = "users", key = "#result.id")
public User createUser(CreateUserRequest request) {
User user = User.builder()
.name(request.getName())
.email(request.getEmail())
.build();
return userRepository.save(user);
}
}
@CacheEvict
@Service
public class UserService {
// Remove single entry from cache
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// Clear entire cache
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
log.info("User cache cleared");
}
// Evict before method execution
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUserBeforeInvocation(Long id) {
// Even if this throws exception, cache entry is removed
userRepository.deleteById(id);
}
}
@Caching (Multiple Operations)
@Service
public class UserService {
@Caching(
put = {
@CachePut(value = "users", key = "#result.id"),
@CachePut(value = "usersByEmail", key = "#result.email")
},
evict = {
@CacheEvict(value = "allUsers", allEntries = true)
}
)
public User updateUser(User user) {
return userRepository.save(user);
}
@Caching(evict = {
@CacheEvict(value = "users", key = "#id"),
@CacheEvict(value = "usersByEmail", allEntries = true),
@CacheEvict(value = "allUsers", allEntries = true)
})
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
Using RedisTemplate
Configuration
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key serializer
template.setKeySerializer(new StringRedisSerializer());
// Value serializer
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(jsonSerializer);
// Hash serializers
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
Direct Redis Operations
@Service
@RequiredArgsConstructor
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
// String operations
public void setValue(String key, Object value, Duration ttl) {
redisTemplate.opsForValue().set(key, value, ttl);
}
public Object getValue(String key) {
return redisTemplate.opsForValue().get(key);
}
// Check if key exists
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
// Delete key
public void delete(String key) {
redisTemplate.delete(key);
}
// Set expiration
public void setExpiration(String key, Duration ttl) {
redisTemplate.expire(key, ttl);
}
// List operations
public void addToList(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
}
public List<Object> getList(String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
// Set operations
public void addToSet(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public Set<Object> getSetMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
// Hash operations
public void setHash(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
public Object getHashValue(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
public Map<Object, Object> getHash(String key) {
return redisTemplate.opsForHash().entries(key);
}
}
Session Caching
Store Sessions in Redis
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 minutes
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("SESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
}
Cache Patterns
Cache-Aside Pattern
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final RedisTemplate<String, Product> redisTemplate;
private static final String CACHE_PREFIX = "product:";
private static final Duration CACHE_TTL = Duration.ofHours(1);
public Product getProduct(Long id) {
String key = CACHE_PREFIX + id;
// Try to get from cache
Product cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached;
}
// Get from database
Product product = productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
// Store in cache
redisTemplate.opsForValue().set(key, product, CACHE_TTL);
return product;
}
public Product updateProduct(Long id, UpdateProductRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
product.setName(request.getName());
product.setPrice(request.getPrice());
Product saved = productRepository.save(product);
// Update cache
String key = CACHE_PREFIX + id;
redisTemplate.opsForValue().set(key, saved, CACHE_TTL);
return saved;
}
public void deleteProduct(Long id) {
productRepository.deleteById(id);
// Remove from cache
String key = CACHE_PREFIX + id;
redisTemplate.delete(key);
}
}
Write-Through Pattern
@Service
@RequiredArgsConstructor
public class UserService {
@CachePut(value = "users", key = "#result.id")
@Transactional
public User createUser(CreateUserRequest request) {
// Both database and cache are updated together
User user = User.builder()
.name(request.getName())
.email(request.getEmail())
.build();
return userRepository.save(user);
}
}
Cache Warming
Preload Cache on Startup
@Component
@RequiredArgsConstructor
@Slf4j
public class CacheWarmer implements ApplicationRunner {
private final ProductService productService;
private final ProductRepository productRepository;
@Override
public void run(ApplicationArguments args) {
log.info("Warming up product cache...");
// Load most popular products into cache
List<Product> popularProducts = productRepository.findTop100ByOrderByViewCountDesc();
popularProducts.forEach(product -> {
productService.getProductById(product.getId()); // This will cache the product
});
log.info("Cache warming completed. {} products cached.", popularProducts.size());
}
}
Scheduled Cache Refresh
@Component
@RequiredArgsConstructor
public class CacheRefreshScheduler {
private final CacheManager cacheManager;
private final ProductService productService;
@Scheduled(fixedRate = 3600000) // Every hour
public void refreshProductCache() {
Cache cache = cacheManager.getCache("products");
if (cache != null) {
cache.clear();
}
// Re-warm cache
productService.warmCache();
}
}
Error Handling
Graceful Degradation
@Service
@RequiredArgsConstructor
@Slf4j
public class ResilientCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final UserRepository userRepository;
public User getUser(Long id) {
String key = "user:" + id;
try {
User cached = (User) redisTemplate.opsForValue().get(key);
if (cached != null) {
return cached;
}
} catch (Exception e) {
log.warn("Redis unavailable, falling back to database: {}", e.getMessage());
}
// Fallback to database
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
// Try to cache
try {
redisTemplate.opsForValue().set(key, user, Duration.ofHours(1));
} catch (Exception e) {
log.warn("Failed to cache user: {}", e.getMessage());
}
return user;
}
}
Custom Cache Error Handler
@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.warn("Cache get error for key {}: {}", key, e.getMessage());
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.warn("Cache put error for key {}: {}", key, e.getMessage());
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.warn("Cache evict error for key {}: {}", key, e.getMessage());
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.warn("Cache clear error: {}", e.getMessage());
}
};
}
}
Monitoring
Cache Statistics
@RestController
@RequestMapping("/api/cache")
@RequiredArgsConstructor
public class CacheController {
private final RedisTemplate<String, Object> redisTemplate;
@GetMapping("/stats")
public Map<String, Object> getCacheStats() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection()
.info();
Map<String, Object> stats = new HashMap<>();
stats.put("used_memory", info.getProperty("used_memory_human"));
stats.put("connected_clients", info.getProperty("connected_clients"));
stats.put("total_connections", info.getProperty("total_connections_received"));
stats.put("keyspace_hits", info.getProperty("keyspace_hits"));
stats.put("keyspace_misses", info.getProperty("keyspace_misses"));
return stats;
}
@GetMapping("/keys")
public Set<String> getKeys(@RequestParam(defaultValue = "*") String pattern) {
return redisTemplate.keys(pattern);
}
}
Summary
| Annotation | Purpose |
|---|---|
@Cacheable | Cache method result |
@CachePut | Update cache |
@CacheEvict | Remove from cache |
@Caching | Multiple operations |
Redis caching dramatically improves application performance when implemented correctly with proper TTL management and error handling.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Spring Boot 3 Virtual Threads: Complete Guide to Java 21 Concurrency
Master virtual threads in Spring Boot 3. Learn configuration, performance benchmarks, when to use them, common pitfalls, and production-ready patterns for high-throughput applications.
Spring BootSpring Security Method-Level Authorization: Complete @PreAuthorize Guide
Master method-level security in Spring Boot. Learn @PreAuthorize, @PostAuthorize, SpEL expressions, custom permissions, and domain object security.
Spring BootSpring Security Architecture: Complete Guide to How Security Works
Master Spring Security internals. Learn FilterChain, SecurityContext, Authentication flow, and how Spring Security protects your application.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.