Caching in Spring Boot Applications
Implement effective caching strategies in Spring Boot applications using various caching providers
Caching in Spring Boot Applications
This guide covers implementing caching in Spring Boot applications using various caching providers like Caffeine, Redis, and EhCache.
Video Tutorial
Learn more about Spring Boot caching in this comprehensive video tutorial:
Prerequisites
<dependencies>
<!-- Spring Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Redis Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- EhCache -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
</dependencies>
Basic Caching Configuration
Enable Caching
@EnableCaching
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("users"),
new ConcurrentMapCache("products"),
new ConcurrentMapCache("orders")
));
return cacheManager;
}
}
Using Cache Annotations
@Service
@CacheConfig(cacheNames = {"users"})
public class UserService {
private final UserRepository userRepository;
@Cacheable(key = "#id")
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
@CachePut(key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(allEntries = true)
public void clearCache() {
// This method will clear all entries in the "users" cache
}
}
Caffeine Cache Implementation
Configuration
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
@Bean
public Caffeine caffeineConfig() {
return Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats();
}
}
Custom Cache Specifications
@Configuration
public class CustomCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
Map<String, CaffeineCache> caches = new HashMap<>();
// User cache configuration
caches.put("users", buildCache("users",
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.MINUTES)));
// Product cache configuration
caches.put("products", buildCache("products",
Caffeine.newBuilder()
.maximumSize(2000)
.expireAfterWrite(1, TimeUnit.HOURS)));
cacheManager.setCaches(caches.values());
return cacheManager;
}
private CaffeineCache buildCache(String name, Caffeine<Object, Object> caffeine) {
return new CaffeineCache(name, caffeine.build());
}
}
Redis Cache Implementation
Configuration
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
factory.setHostName("localhost");
factory.setPort(6379);
return factory;
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(customCacheConfigurations())
.build();
}
private Map<String, RedisCacheConfiguration> customCacheConfigurations() {
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("users", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)));
configMap.put("products", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)));
return configMap;
}
}
Redis Cache Service
@Service
@CacheConfig(cacheNames = "products")
public class ProductService {
private final ProductRepository productRepository;
private final RedisTemplate<String, Product> redisTemplate;
@Cacheable(key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
@CachePut(key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
public List<Product> getProductsByCategory(String category) {
String cacheKey = "category:" + category;
ValueOperations<String, List<Product>> ops = redisTemplate.opsForValue();
List<Product> products = ops.get(cacheKey);
if (products == null) {
products = productRepository.findByCategory(category);
ops.set(cacheKey, products, 30, TimeUnit.MINUTES);
}
return products;
}
}
Cache Synchronization
Cache Sync Service
@Service
public class CacheSyncService {
private final CacheManager cacheManager;
private final KafkaTemplate<String, CacheEvent> kafkaTemplate;
@KafkaListener(topics = "cache-events")
public void handleCacheEvent(CacheEvent event) {
switch (event.getType()) {
case UPDATE:
updateCache(event);
break;
case DELETE:
evictCache(event);
break;
case CLEAR:
clearCache(event);
break;
}
}
private void updateCache(CacheEvent event) {
Cache cache = cacheManager.getCache(event.getCacheName());
if (cache != null) {
cache.put(event.getKey(), event.getValue());
}
}
private void evictCache(CacheEvent event) {
Cache cache = cacheManager.getCache(event.getCacheName());
if (cache != null) {
cache.evict(event.getKey());
}
}
private void clearCache(CacheEvent event) {
Cache cache = cacheManager.getCache(event.getCacheName());
if (cache != null) {
cache.clear();
}
}
}
Cache Monitoring
Cache Metrics
@Component
public class CacheMetrics {
private final MeterRegistry registry;
public CacheMetrics(MeterRegistry registry) {
this.registry = registry;
}
public void recordCacheHit(String cacheName) {
registry.counter("cache.hits",
"cache", cacheName).increment();
}
public void recordCacheMiss(String cacheName) {
registry.counter("cache.misses",
"cache", cacheName).increment();
}
public void recordCacheEviction(String cacheName) {
registry.counter("cache.evictions",
"cache", cacheName).increment();
}
public void recordCacheSize(String cacheName, long size) {
registry.gauge("cache.size",
Tags.of("cache", cacheName),
size);
}
}
Cache Statistics
@Service
public class CacheStatsService {
private final CacheManager cacheManager;
private final CacheMetrics cacheMetrics;
@Scheduled(fixedRate = 60000) // Every minute
public void reportCacheStats() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache instanceof CaffeineCache) {
CaffeineCache caffeineCache = (CaffeineCache) cache;
com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache =
caffeineCache.getNativeCache();
CacheStats stats = nativeCache.stats();
cacheMetrics.recordCacheHit(cacheName);
cacheMetrics.recordCacheMiss(cacheName);
cacheMetrics.recordCacheEviction(cacheName);
cacheMetrics.recordCacheSize(cacheName, nativeCache.estimatedSize());
}
});
}
}
Best Practices
-
Cache Configuration
- Set appropriate TTL
- Configure cache size limits
- Use proper serialization
- Enable statistics
-
Cache Keys
- Use meaningful keys
- Consider key complexity
- Handle null values
- Use key generators
-
Cache Eviction
- Implement eviction policies
- Handle cache invalidation
- Use cache clearing strategies
- Monitor eviction rates
-
Performance
- Monitor cache hit rates
- Optimize cache size
- Use appropriate cache providers
- Implement cache warming
Common Patterns
- Cache-Aside Pattern
@Service
public class CacheAsideService {
private final Cache cache;
private final DataService dataService;
public Object getData(String key) {
// Try cache first
Object value = cache.get(key);
if (value != null) {
return value;
}
// Cache miss - get from source
value = dataService.getData(key);
// Store in cache
if (value != null) {
cache.put(key, value);
}
return value;
}
}
- Write-Through Pattern
@Service
public class WriteThroughService {
private final Cache cache;
private final DataService dataService;
public void saveData(String key, Object value) {
// Save to data store
dataService.saveData(key, value);
// Update cache
cache.put(key, value);
}
}
Conclusion
Effective caching in Spring Boot requires:
- Proper cache configuration
- Appropriate cache providers
- Cache synchronization
- Monitoring and metrics
- Following best practices
For more Spring Boot topics, check out: