# Spring Boot Cache Abstraction - Examples This document provides concrete, progressive examples demonstrating Spring Boot caching patterns from basic to advanced scenarios. ## Example 1: Basic Product Caching A simple e-commerce scenario with product lookup caching. ### Domain Model ```java @Getter @ToString @EqualsAndHashCode(of = "id") @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder public class Product { private Long id; private String name; private BigDecimal price; private Integer stock; private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` ### Service with @Cacheable ```java @Service @CacheConfig(cacheNames = "products") @RequiredArgsConstructor @Slf4j public class ProductService { private final ProductRepository productRepository; @Cacheable public Product getProductById(Long id) { log.info("Fetching product {} from database", id); return productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found")); } @Cacheable(key = "#name") public Product getProductByName(String name) { log.info("Fetching product by name: {}", name); return productRepository.findByName(name) .orElseThrow(() -> new ResourceNotFoundException("Product not found")); } @CachePut(key = "#product.id") public Product updateProduct(Product product) { log.info("Updating product {}", product.getId()); return productRepository.save(product); } @CacheEvict public void deleteProduct(Long id) { log.info("Deleting product {}", id); productRepository.deleteById(id); } @CacheEvict(allEntries = true) public void refreshAllProducts() { log.info("Refreshing all product cache"); } } ``` ### Test Example ```java @SpringBootTest @Testcontainers class ProductServiceCacheTest { @Container static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); @Autowired private ProductService productService; @SpyBean private ProductRepository productRepository; @Test void shouldCacheProductAfterFirstCall() { // Given Product product = Product.builder() .id(1L) .name("Laptop") .price(BigDecimal.valueOf(999.99)) .stock(10) .build(); when(productRepository.findById(1L)).thenReturn(Optional.of(product)); // When - First call Product result1 = productService.getProductById(1L); // Then - Verify database was called verify(productRepository, times(1)).findById(1L); assertThat(result1).isEqualTo(product); // When - Second call (should hit cache) Product result2 = productService.getProductById(1L); // Then - Database not called again verify(productRepository, times(1)).findById(1L); // Still 1x assertThat(result2).isEqualTo(result1); } @Test void shouldEvictCacheOnDelete() { // Given Product product = Product.builder() .id(1L) .name("Laptop") .price(BigDecimal.valueOf(999.99)) .build(); when(productRepository.findById(1L)).thenReturn(Optional.of(product)); // Populate cache productService.getProductById(1L); verify(productRepository, times(1)).findById(1L); // When - Delete (evicts cache) productService.deleteProduct(1L); // Then - Next call should query database again when(productRepository.findById(1L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> productService.getProductById(1L)) .isInstanceOf(ResourceNotFoundException.class); verify(productRepository, times(2)).findById(1L); } } ``` --- ## Example 2: Conditional Caching with Business Logic Cache products only under specific conditions (e.g., only expensive items). ```java @Service @RequiredArgsConstructor @Slf4j public class PremiumProductService { private final ProductRepository productRepository; @Cacheable( value = "premiumProducts", condition = "#price > 500", // Cache only items over 500 unless = "#result == null" ) public Product getPremiumProduct(Long id, BigDecimal price) { log.info("Fetching premium product {} (price: {})", id, price); return productRepository.findById(id) .orElse(null); } @CachePut( value = "discountedProducts", key = "#product.id", condition = "#product.price < 50" // Cache only discounted items ) public Product updateDiscountedProduct(Product product) { log.info("Updating discounted product {}", product.getId()); return productRepository.save(product); } } ``` **Test:** ```java @Test void shouldCachePremiumProductsOnly() { // Given - Cheap product Product cheapProduct = Product.builder() .id(1L) .name("Budget Item") .price(BigDecimal.valueOf(29.99)) .build(); // When - Call with cheap price (won't cache due to condition) Product result = premiumProductService.getPremiumProduct(1L, BigDecimal.valueOf(29.99)); // Then - Result should be cached (condition false, so not cached) verify(productRepository, times(1)).findById(1L); // Second call should hit DB again premiumProductService.getPremiumProduct(1L, BigDecimal.valueOf(29.99)); verify(productRepository, times(2)).findById(1L); } ``` --- ## Example 3: Multiple Caches and @Caching Handle complex scenarios with multiple cache operations. ```java @Service @RequiredArgsConstructor @Slf4j public class InventoryService { private final ProductRepository productRepository; @Caching( cacheable = @Cacheable("inventoryCache"), put = { @CachePut(value = "stockCache", key = "#id"), @CachePut(value = "priceCache", key = "#id") } ) public Product getInventoryDetails(Long id) { log.info("Fetching inventory details for {}", id); return productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found")); } @Caching( evict = { @CacheEvict("inventoryCache"), @CacheEvict("stockCache"), @CacheEvict("priceCache") } ) public void reloadInventory(Long id) { log.info("Reloading inventory for {}", id); // Trigger inventory sync from external system } } ``` --- ## Example 4: Programmatic Cache Management Manually managing caches for advanced scenarios. ```java @Component @RequiredArgsConstructor @Slf4j public class CacheManagementService { private final CacheManager cacheManager; public void evictProductCache(Long productId) { Cache cache = cacheManager.getCache("products"); if (cache != null) { cache.evict(productId); log.info("Evicted product {} from cache", productId); } } public void clearAllCaches() { cacheManager.getCacheNames().forEach(cacheName -> { Cache cache = cacheManager.getCache(cacheName); if (cache != null) { cache.clear(); log.info("Cleared cache: {}", cacheName); } }); } public T getOrCompute(String cacheName, Object key, Callable valueLoader) { Cache cache = cacheManager.getCache(cacheName); if (cache == null) { log.warn("Cache {} not found", cacheName); return null; } Cache.ValueWrapper wrapper = cache.get(key); if (wrapper != null) { return (T) wrapper.get(); } try { T value = valueLoader.call(); cache.put(key, value); return value; } catch (Exception e) { log.error("Error computing cache value", e); throw new RuntimeException(e); } } } ``` --- ## Example 5: Cache Warming/Preloading Populate cache with frequently accessed data at startup. ```java @Component @RequiredArgsConstructor @Slf4j public class CacheWarmupService implements InitializingBean { private final ProductService productService; private final ProductRepository productRepository; @Override public void afterPropertiesSet() { warmupCache(); } private void warmupCache() { log.info("Warming up product cache..."); // Load top 100 products List topProducts = productRepository.findTop100ByOrderByPopularityDesc(); topProducts.forEach(product -> { try { productService.getProductById(product.getId()); } catch (Exception e) { log.warn("Failed to warm cache for product {}", product.getId(), e); } }); log.info("Cache warmup completed. {} products cached", topProducts.size()); } } ``` --- ## Example 6: Cache Statistics and Monitoring Track cache performance metrics. ```java @Component @RequiredArgsConstructor @Slf4j public class CacheStatsService { private final CacheManager cacheManager; @Scheduled(fixedRate = 60000) // Every minute public void logCacheStats() { cacheManager.getCacheNames().forEach(cacheName -> { Cache cache = cacheManager.getCache(cacheName); if (cache != null && cache.getNativeCache() instanceof ConcurrentMapCache) { ConcurrentMapCache concreteCache = (ConcurrentMapCache) cache.getNativeCache(); log.info("Cache [{}] - Size: {}", cacheName, concreteCache.getStore().size()); } }); } @GetMapping("/cache/stats") public ResponseEntity> getCacheStatistics() { Map stats = new HashMap<>(); cacheManager.getCacheNames().forEach(cacheName -> { Cache cache = cacheManager.getCache(cacheName); if (cache != null) { CacheStats cacheStats = new CacheStats( cacheName, getCacheSize(cache), LocalDateTime.now() ); stats.put(cacheName, cacheStats); } }); return ResponseEntity.ok(stats); } private int getCacheSize(Cache cache) { if (cache.getNativeCache() instanceof ConcurrentMap) { return ((ConcurrentMap) cache.getNativeCache()).size(); } return 0; } } @Data @NoArgsConstructor @AllArgsConstructor class CacheStats { private String cacheName; private int size; private LocalDateTime timestamp; } ``` --- ## Example 7: TTL-Based Cache with Scheduled Eviction Expire cache entries after a specific time. ```java @Configuration @EnableCaching @EnableScheduling public class CacheConfig { @Bean public CacheManager cacheManager() { return new ConcurrentMapCacheManager("products", "users", "orders"); } } @Component @RequiredArgsConstructor @Slf4j public class CacheExpirationService { private final CacheManager cacheManager; private final Map cacheExpirations = new ConcurrentHashMap<>(); public void setExpiration(String cacheName, Object key, Duration duration) { String expirationKey = cacheName + ":" + key; cacheExpirations.put(expirationKey, LocalDateTime.now().plus(duration)); log.info("Set cache expiration for {} after {}", expirationKey, duration); } @Scheduled(fixedRate = 5000) // Check every 5 seconds public void evictExpiredEntries() { LocalDateTime now = LocalDateTime.now(); cacheExpirations.entrySet() .removeIf(entry -> { if (now.isAfter(entry.getValue())) { String[] parts = entry.getKey().split(":"); String cacheName = parts[0]; String key = parts[1]; Cache cache = cacheManager.getCache(cacheName); if (cache != null) { cache.evict(key); log.info("Evicted expired cache entry: {}", entry.getKey()); } return true; } return false; }); } } ``` --- ## Example 8: Cache Invalidation Pattern with Events Use domain events to invalidate cache across services. ```java public class ProductUpdatedEvent extends ApplicationEvent { private final Long productId; private final String changeType; // UPDATED, DELETED, CREATED public ProductUpdatedEvent(Object source, Long productId, String changeType) { super(source); this.productId = productId; this.changeType = changeType; } } @Component @RequiredArgsConstructor @Slf4j public class ProductService { private final ProductRepository productRepository; private final ApplicationEventPublisher eventPublisher; public Product updateProduct(Long id, UpdateProductRequest request) { Product product = productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found")); product.setName(request.getName()); product.setPrice(request.getPrice()); Product updated = productRepository.save(product); // Publish event to invalidate cache eventPublisher.publishEvent(new ProductUpdatedEvent(this, id, "UPDATED")); return updated; } } @Component @RequiredArgsConstructor @Slf4j public class CacheInvalidationListener { private final CacheManager cacheManager; @EventListener public void onProductUpdated(ProductUpdatedEvent event) { log.info("Invalidating cache for product {}", event.getProductId()); Cache productsCache = cacheManager.getCache("products"); if (productsCache != null) { productsCache.evict(event.getProductId()); } Cache productsListCache = cacheManager.getCache("productsList"); if (productsListCache != null) { productsListCache.clear(); } } } ``` --- ## Example 9: Distributed Caching with Caffeine Using Caffeine for local caching with advanced features. ```java @Configuration @EnableCaching public class CaffeineCacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager("products", "users"); cacheManager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .recordStats()); return cacheManager; } } @Component @RequiredArgsConstructor public class CacheMetricsService { private final CacheManager cacheManager; @GetMapping("/cache/metrics") public ResponseEntity> getCacheMetrics() { Map metrics = new HashMap<>(); cacheManager.getCacheNames().forEach(cacheName -> { Cache cache = cacheManager.getCache(cacheName); if (cache != null && cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) { com.github.benmanes.caffeine.cache.Cache caffeineCache = (com.github.benmanes.caffeine.cache.Cache) cache.getNativeCache(); com.github.benmanes.caffeine.cache.stats.CacheStats stats = caffeineCache.stats(); metrics.put(cacheName, Map.of( "hitCount", stats.hitCount(), "missCount", stats.missCount(), "hitRate", stats.hitRate(), "size", caffeineCache.estimatedSize() )); } }); return ResponseEntity.ok(metrics); } } ``` --- ## Example 10: Testing Cache-Related Scenarios ```java @SpringBootTest class CacheIntegrationTest { @Autowired private ProductService productService; @Autowired private CacheManager cacheManager; @MockBean private ProductRepository productRepository; @Test void shouldDemonstrateCachingLifecycle() { // Given Product product = Product.builder() .id(1L) .name("Test Product") .price(BigDecimal.TEN) .build(); when(productRepository.findById(1L)).thenReturn(Optional.of(product)); // Verify cache is empty Cache cache = cacheManager.getCache("products"); assertThat(cache.get(1L)).isNull(); // First call - populates cache Product result1 = productService.getProductById(1L); verify(productRepository, times(1)).findById(1L); // Cache is now populated assertThat(cache.get(1L)).isNotNull(); // Second call - uses cache Product result2 = productService.getProductById(1L); verify(productRepository, times(1)).findById(1L); // Still 1x assertThat(result1).isEqualTo(result2); // Manual eviction cache.evict(1L); assertThat(cache.get(1L)).isNull(); // Next call queries database again Product result3 = productService.getProductById(1L); verify(productRepository, times(2)).findById(1L); } } ```