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

618 lines
17 KiB
Markdown

# 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> 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.
```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<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.
```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<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.
```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<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.
```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<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);
}
}
```
---
## 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);
}
}
```