Cache Annotations

Enable Caching

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@SpringBootApplication
@EnableCaching
public class ShopEaseApplication { ... }

Without any provider library Spring Boot uses a simple ConcurrentHashMap cache (no TTL), fine for learning.

@Cacheable, @CachePut, @CacheEvict

@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository repo;

    @Cacheable(cacheNames = "products", key = "#id")
    public ProductDto findById(Long id) {
        log.info("Loading product {} from DB", id);
        return repo.findById(id).map(ProductDto::from)
                   .orElseThrow(() -> new ProductNotFoundException(id));
    }

    @Cacheable(cacheNames = "productsByCategory", key = "#category", unless = "#result.isEmpty()")
    public List<ProductDto> findByCategory(String category) { ... }

    @CachePut(cacheNames = "products", key = "#result.id()")      // always runs, refreshes the entry
    @CacheEvict(cacheNames = "productsByCategory", allEntries = true)
    public ProductDto update(Long id, UpdateProductRequest req) { ... }

    @CacheEvict(cacheNames = "products", key = "#id")
    public void delete(Long id) { repo.deleteById(id); }
}

Hit and Miss in the Logs

Example

Call the same product twice:

curl -s localhost:8080/api/products/3
curl -s localhost:8080/api/products/3

Example output

INFO  ProductService : Loading product 3 from DB      <- first call (miss)
{"id":3,"name":"Pixel 9","price":54999.00,"stock":12,"category":"mobiles"}
{"id":3,"name":"Pixel 9","price":54999.00,"stock":12,"category":"mobiles"}   <- second call, no DB log (hit)
AttributeUse
keySpEL for the key: #id, #req.sku, #root.methodName
conditioncache only if true before the call: condition = "#id > 0"
unlessskip caching based on the result: unless = "#result == null"
sync = trueonly one thread loads a missing key (prevents a stampede)

Note: Cached return values should be immutable DTOs (records), not JPA entities: entities hold lazy proxies and session state.

Practice

Exercise: Cache getCartTotal(Long userId) in cache cartTotals, and evict that user's entry whenever addToCart(Long userId, AddItemRequest req) runs.

Show answer
@Cacheable(cacheNames = "cartTotals", key = "#userId")
public BigDecimal getCartTotal(Long userId) { ... }

@CacheEvict(cacheNames = "cartTotals", key = "#userId")
public CartDto addToCart(Long userId, AddItemRequest req) { ... }