Spring Boot Caching
Spring Boot provides multiple ways for caching of data from in memory cache to distributed cache support. Here we are discussing spring boot caching with redis.
Redis integration with Spring Boot provides a powerful, high-performance caching solution that significantly improves application performance and scalability.
Why Redis for Caching?
Redis isn't just any cache; it's a superb choice for modern applications because it's:
โก Ultra-Fast: Being an in-memory data store, reads and writes are exceptionally fast.
๐ Distributed & Scalable: It runs as a separate server, allowing multiple instances of your app to share one consistent cache, and can be clustered for high availability.
๐งฐ Rich with Features: Redis offers Time-To-Live (TTL) settings, smart eviction policies, and versatile data structures that go far beyond simple key-value pairs.
Level 1: Core Redis Configuration ๐
Step 1: Add the Dependencies
You'll need two key starters in your pom.xml
to get Spring's caching abstraction and Redis support.
XML
<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>
Step 2: Configure Redis Connection
Next, tell your application how to connect to your Redis server. Add these properties to your application.properties
file.
Properties
# Redis server host
spring.data.redis.host=localhost
# Redis server port
spring.data.redis.port=6379
# Optional: Add a password if your Redis is secured
# spring.data.redis.password=yourpassword
# Connection timeout
spring.data.redis.timeout=2000ms
# Lettuce connection pool settings
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
# application.properties for a Redis Cluster for distributed redis cluster
spring.data.redis.cluster.nodes=redis1:6379,redis2:6379,redis3:6379
spring.data.redis.cluster.max-redirects=3
Step 3: Enable Caching
The final step is to activate Spring's caching capabilities. Just add the @EnableCaching
annotation to your main application class.
Java
@SpringBootApplication
@EnableCaching // <-- This little annotation does all the magic! โจ
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Level 2: The Magic of Cache Annotations โจ
Spring's cache abstraction provides simple yet powerful annotations to manage your cache without writing boilerplate code.
@Cacheable
:
@Cacheable
: Key Idea: If a cache entry exists, return it. If not, run the method, cache the result, and then return it.
@Service
public class ProductService {
// The result of this method will be stored in a cache named "products"
// with a key equal to the 'id' parameter.
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
System.out.println("Fetching product from DB for ID: " + id);
return productRepository.findById(id).orElse(null);
}
@Cacheable(value = "products", key = "#category + '_' + #page")
public List<Product> findByCategory(String category, int page) {
return productRepository.findByCategoryWithPagination(category, page);
}
}
@CachePut
: Always Update the Cache
@CachePut
: Always Update the CacheThe @CachePut
annotation always executes the method and updates the cache with the result:
Key Idea: Always run the method and update the cache with the new result.
@Service
public class ProductService {
// This method will always execute, and the returned Product
// will update the entry in the "products" cache.
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
System.out.println("Updating product in DB and cache...");
return productRepository.save(product);
}
}
@CacheEvict
: Clearing Out the Old
@CacheEvict
: Clearing Out the OldThe @CacheEvict
annotation removes entries from the cache:
Key Idea: Run the method and remove an entry from the cache.
@Service
public class ProductService {
// Removes the product with the matching 'id' from the cache.
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
System.out.println("Deleting product from DB and cache...");
productRepository.deleteById(id);
}
// Use allEntries=true to clear the entire cache.
// Useful for bulk update scenarios.
@CacheEvict(value = "products", allEntries = true)
public void clearAllProductCache() {
System.out.println("Clearing all products from the cache!");
}
}
@Caching
and @CacheConfig
:
@Caching
and @CacheConfig
: Sometimes you need to perform multiple cache operations at once. @Caching
lets you combine them. To avoid repeating value = "products"
everywhere, you can use @CacheConfig
at the class level.
@Service
@CacheConfig(cacheNames = "products") // Common cache settings for the class
public class ProductService {
@Cacheable(key = "#id")
public Product findById(Long id) { /* ... */ }
// Evict a single product AND a related category cache
@Caching(evict = {
@CacheEvict(key = "#product.id"),
@CacheEvict(value = "productsByCategory", key = "#product.category")
})
public Product updateProductWithCategoryChange(Product product) {
return productRepository.save(product);
}
}
Key Generators
By default, Spring creates a key from your method parameters. For more complex scenarios, you can use Spring Expression Language (SpEL) or create a custom KeyGenerator
.
SpEL-Based Keys
SpEL gives you incredible flexibility right inside the annotation. We can use class methods, variables and method response from anther class .
// Key = "user_123_ADMIN"
@Cacheable(value = "users", key = "'user_' + #user.id + '_' + #user.role.name")
public UserProfile getUserProfile(User user) { /* ... */ }
private final String CACHE_NAME = "quickReplies";
// calling static method of class as key
private final String CACHE_KEY = "T(com.glut.app.util.JwtUtil).getLoggedInUserName()";
@Cacheable(value = CACHE_NAME, key = CACHE_KEY)
Custom KeyGenerator
KeyGenerator
For a reusable, complex key strategy, implement the KeyGenerator
interface.
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// Your custom logic to build a unique key string
return target.getClass().getSimpleName() + "_"
+ method.getName() + "_"
+ StringUtils.arrayToDelimitedString(params, "_");
}
}
// Then use it in your annotation
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public Product findProductByComplexCriteria(String category, String brand) { /* ... */ }
Level 3: Custom Configuration โ๏ธ
Different serialization approaches offer various trade-offs:
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// Set a default 10-minute TTL for all cache entries
.entryTtl(Duration.ofMinutes(10))
// Don't cache 'null' values
.disableCachingNullValues()
// Use String serializer for keys
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
// Use JSON serializer for values
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// Custom configurations for specific caches
Map<String, RedisCacheConfiguration> cacheConfigurations = Map.of(
"userCache", defaultConfig.entryTtl(Duration.ofMinutes(20)),
"productCache", defaultConfig.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigurations)
//Cache operations will only be executed if the surrounding database transaction commits successfully
.transactionAware() // Make cache operations transactional!
.build();
}
}
// Custom configurations for specific caches
@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
return (builder) -> builder
.withCacheConfiguration("userCache", // Specific config for "userCache"
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(20)))
.withCacheConfiguration("productCache", // Specific config for "productCache"
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)));
}
//Type-Safe Serialization
//For specific object types, configure dedicated serializers:
@Bean
public Jackson2JsonRedisSerializer<User> userSerializer() {
Jackson2JsonRedisSerializer<User> serializer =
new Jackson2JsonRedisSerializer<>(User.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
return serializer;
}
Level 4: Production-Ready Patterns ๐
Now let's level up with advanced patterns to make your caching robust, performant, and reliable in a production environment.
Cache Eviction Strategies and Policies
Configure Redis eviction policies for memory management:
@Bean
public RedisConnectionFactory connectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
// Set eviction policy through Redis configuration
return factory;
}
Redis supports several eviction policies:
allkeys-lru
: Remove least recently used keysallkeys-lfu
: Remove least frequently used keysvolatile-lru
: Remove LRU keys with expiration setvolatile-ttl
: Remove keys with shortest TTL
Programmatic Cache Management
Direct cache manipulation when needed:
@Service
public class CacheManagementService {
@Autowired
private CacheManager cacheManager;
public void evictUserCache(Long userId) {
Cache userCache = cacheManager.getCache("users");
if (userCache != null) {
userCache.evict(userId);
}
}
public void clearAllCaches() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cache.clear();
}
});
}
}
Conditional Eviction
Implement conditional cache eviction:
@CacheEvict(value = "products", key = "#product.id", condition = "#product.status == 'DISCONTINUED'")
public Product updateProductStatus(Product product) {
return productRepository.save(product);
}
@CacheEvict(value = "users", key = "#user.id", unless = "#user.isVip == true")
public void updateUser(User user) {
userRepository.save(user);
}
Distributed Caching Scenarios
Multi-Instance Cache Synchronization
Redis provides distributed caching capabilities that ensure cache consistency across multiple application instances:
@Configuration
public class DistributedCacheConfig {
@Bean
public RedisCacheManager distributedCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
.prefixCacheNameWith("app1::")
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
Redis Cluster Configuration
For high-availability scenarios, configure Redis Cluster:
spring.data.redis.cluster.nodes=redis1:6379,redis2:6379,redis3:6379
spring.data.redis.cluster.max-redirects=3
spring.data.redis.lettuce.cluster.refresh.adaptive=true
spring.data.redis.lettuce.cluster.refresh.period=60s
@Configuration
public class RedisClusterConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration clusterConfig =
new RedisClusterConfiguration(Arrays.asList(
"redis1:6379", "redis2:6379", "redis3:6379"));
return new LettuceConnectionFactory(clusterConfig);
}
}
Redis Cluster automatically partitions data across multiple nodes and provides fault tolerance
Error Handling & Resilience
What if your Redis server goes down? By default, your application will throw an exception and fail the request. You can define a custom CacheErrorHandler
to simply log the error and allow the application to proceed by fetching data from the database, ensuring your app remains resilient.
@Configuration
public class CacheErrorHandlingConfig extends CachingConfigurerSupport {
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException exception,
Cache cache, Object key) {
log.warn("Cache get error for key {}: {}", key, exception.getMessage());
// Continue without cache
}
@Override
public void handleCachePutError(RuntimeException exception,
Cache cache, Object key, Object value) {
log.warn("Cache put error for key {}: {}", key, exception.getMessage());
// Continue without caching
}
@Override
public void handleCacheEvictError(RuntimeException exception,
Cache cache, Object key) {
log.warn("Cache evict error for key {}: {}", key, exception.getMessage());
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
log.warn("Cache clear error: {}", exception.getMessage());
}
};
}
// Handle serialization problems effectively
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Handle serialization errors gracefully
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer() {
@Override
public byte[] serialize(Object source) throws SerializationException {
try {
return super.serialize(source);
} catch (Exception e) {
log.warn("Serialization failed for object: {}", source, e);
return new JdkSerializationRedisSerializer().serialize(source);
}
}
});
return template;
}
}
//This ensures application resilience when Redis is unavailable
Performance Optimization and Monitoring
Cache Statistics and Metrics
Enable cache statistics for monitoring:
spring.cache.redis.enable-statistics=true
management.endpoints.web.exposure.include=metrics,prometheus
management.endpoint.metrics.enabled=true
@Component
public class CacheMetricsConfig {
@Autowired
private CacheMetricsRegistrar cacheMetricsRegistrar;
@Autowired
private CacheManager cacheManager;
@EventListener(ApplicationStartedEvent.class)
public void registerCacheMetrics() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
cacheMetricsRegistrar.bindCacheToRegistry(cache);
}
});
}
// additional config
@Autowired
private MeterRegistry meterRegistry;
@EventListener
@Async
public void handleCacheHit(CacheHitEvent event) {
Counter.builder("cache.hit")
.tag("cache", event.getCacheName())
.register(meterRegistry)
.increment();
}
@EventListener
@Async
public void handleCacheMiss(CacheMissEvent event) {
Counter.builder("cache.miss")
.tag("cache", event.getCacheName())
.register(meterRegistry)
.increment();
}
}
This configuration exposes cache hit/miss ratios, cache size, and eviction metrics through Spring Boot Actuator
You can now access the /actuator/metrics/cache.gets
endpoint to see your hit/miss ratio. A high hit ratio (e.g., >80%) means your cache is working well!
Cache Warming Strategies
Implement cache warming for critical data:
@Component
public class CacheWarmupService {
@Autowired
private ProductService productService;
@EventListener(ApplicationReadyEvent.class)
public void warmupCache() {
log.info("Starting cache warmup...");
// Warm up popular products
List<Long> popularProductIds = getPopularProductIds();
popularProductIds.parallelStream()
.forEach(productService::findById);
log.info("Cache warmup completed for {} products", popularProductIds.size());
}
@Scheduled(fixedRate = 300000) // 5 minutes
public void refreshCriticalData() {
// Refresh time-sensitive cached data
criticalDataService.refreshCache();
}
}
Advanced Use Cases
Cache Warming: Pre-load critical data into the cache on application startup using an
@EventListener(ApplicationReadyEvent.class)
to ensure fast responses from the very first request.Multi-Level Caching: Combine a fast, in-memory local cache (like Caffeine) with a distributed Redis cache. This provides lightning-fast reads for the hottest data while still offering distributed consistency.
Session Management: Offload HTTP session storage to Redis using
spring-session-data-redis
to enable scalable, stateless application instances.
Level 5: Security & Testing ๐ก๏ธ
Finally, let's secure our connections and ensure our caching logic is bug-free.
Securing Your Redis Connection
In production, you should always secure your Redis instance with a password and SSL/TLS.
Properties
# Enable password authentication
spring.data.redis.password=your-very-secure-password
# Enable SSL for the connection
spring.data.redis.ssl.enabled=true
Testing Your Cache Logic with Testcontainers
Never guess if your caching worksโtest it! Testcontainers makes it incredibly easy to spin up a real Redis container for your integration tests. This ensures your code works with a genuine Redis instance.
Java
@SpringBootTest
@Testcontainers // Enable Testcontainers support
class RedisCacheIntegrationTest {
@Container // Start a Redis container for the test
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource // Dynamically set the Redis host and port for the test
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", redis::getFirstMappedPort);
}
@Autowired
private ProductService productService;
@Test
void testCachingBehavior() {
// First call - should be a cache miss
Product product1 = productService.findById(1L);
// Second call - should be a cache hit!
Product product2 = productService.findById(1L);
// Assert that the object is the same, proving it came from the cache
assertThat(product2).isSameAs(product1);
}
}
Conclusion ๐
You've made it! You now have a solid understanding of how to implement powerful, performant, and resilient caching in your Spring Boot applications using Redis.
We've covered:
The Basics: Setup and simple annotations like
@Cacheable
.Customization: Configuring TTL, serialization, and key generation.
Production Patterns: Distributed clustering, transactions, error handling, and monitoring.
Testing & Security: Writing reliable tests and securing your instance.
By applying these patterns, you can take your application's performance to the next level. Happy coding!
Last updated