CtrlK
BlogDocsLog inGet started
Tessl Logo

giuseppe-trisciuoglio/developer-kit

Comprehensive developer toolkit providing reusable skills for Java/Spring Boot, TypeScript/NestJS/React/Next.js, Python, PHP, AWS CloudFormation, AI/RAG, DevOps, and more.

82

Quality

82%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Risky

Do not use without reviewing

Validation failed for skills in this tile
One or more skills have errors that need to be fixed before they can move to Implementation and Discovery review.
Overview
Quality
Evals
Security
Files

cache-examples.mdplugins/developer-kit-java/skills/spring-boot-cache/references/

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

Example 10: Testing Cache-Related Scenarios

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

plugins

developer-kit-java

skills

README.md

tile.json