Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot/spring-boot-cache/references/cache-examples.md
2025-11-29 18:28:30 +08:00

17 KiB

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

@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

@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

@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).

@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:

@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.

@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.

@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> T getOrCompute(String cacheName, Object key, Callable<T> 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.

@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<Product> 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.

@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<Map<String, CacheStats>> getCacheStatistics() {
        Map<String, CacheStats> 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.

@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<String, LocalDateTime> 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.

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.

@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<Map<String, Object>> getCacheMetrics() {
        Map<String, Object> 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);
    }
}

@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);
    }
}