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

  1. Cache Configuration

    • Set appropriate TTL
    • Configure cache size limits
    • Use proper serialization
    • Enable statistics
  2. Cache Keys

    • Use meaningful keys
    • Consider key complexity
    • Handle null values
    • Use key generators
  3. Cache Eviction

    • Implement eviction policies
    • Handle cache invalidation
    • Use cache clearing strategies
    • Monitor eviction rates
  4. Performance

    • Monitor cache hit rates
    • Optimize cache size
    • Use appropriate cache providers
    • Implement cache warming

Common Patterns

  1. 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;
    }
}
  1. 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: