java
43 lines · 7 steps
How Spring cache annotations keep data fresh
A Spring service uses @Cacheable and @CacheEvict to serve reads from cache while invalidating stale entries on writes.
Explained by
highlit
1@Service
2public class ProductCatalogService {
3
4 private final ProductRepository productRepository;
5 private final PricingClient pricingClient;
6
7 public ProductCatalogService(ProductRepository productRepository, PricingClient pricingClient) {
8 this.productRepository = productRepository;
9 this.pricingClient = pricingClient;
10 }
11
12 @Cacheable(cacheNames = "products", key = "#productId")
13 public ProductView getProduct(long productId) {
14 Product product = productRepository.findById(productId)
15 .orElseThrow(() -> new ProductNotFoundException(productId));
16 Money price = pricingClient.resolveCurrentPrice(product.getSku());
17 return ProductView.from(product, price);
18 }
19
20 @Cacheable(cacheNames = "productsByCategory", key = "#category", unless = "#result.isEmpty()")
21 public List<ProductView> getProductsByCategory(String category) {
22 return productRepository.findByCategory(category).stream()
23 .map(p -> ProductView.from(p, pricingClient.resolveCurrentPrice(p.getSku())))
24 .toList();
25 }
26
27 @Caching(evict = {
28 @CacheEvict(cacheNames = "products", key = "#result.id"),
29 @CacheEvict(cacheNames = "productsByCategory", key = "#command.category")
30 })
31 @Transactional
32 public Product updateProduct(UpdateProductCommand command) {
33 Product product = productRepository.findById(command.productId())
34 .orElseThrow(() -> new ProductNotFoundException(command.productId()));
35 product.rename(command.name());
36 product.recategorize(command.category());
37 return productRepository.save(product);
38 }
39
40 @CacheEvict(cacheNames = {"products", "productsByCategory"}, allEntries = true)
41 public void invalidateAll() {
42 }
43}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Cache reads with @Cacheable and evict on writes so callers never see stale product data.
- 2SpEL expressions like key and unless let you shape cache keys and skip caching empty results.
- 3Grouping multiple @CacheEvict under @Caching keeps two related caches consistent from a single write.
Related explainers
java
@RestController @RequestMapping("/api/users") public class UserController {
Bean Validation in a Spring REST controller
validation
rest-api
exception-handling
Intermediate
9 steps
python
from django.core.cache import cache from rest_framework.throttling import SimpleRateThrottle
A login rate throttle in Django REST Framework
rate-limiting
caching
throttling
Intermediate
8 steps
java
@Service public class OrderService { private final OrderRepository orderRepository;
Decoupling side effects with Spring events
event-driven
transactions
decoupling
Intermediate
8 steps
java
public Map<Long, List<Order>> ordersByCustomer(List<Order> orders) { return orders.stream() .collect(Collectors.groupingBy(Order::getCustomerId)); }
Grouping streams with Java Collectors
streams
grouping
collectors
Intermediate
5 steps
typescript
import { Directive, Input, TemplateRef,
Building a structural *appUnless directive in Angular
structural-directive
template-rendering
dependency-injection
Intermediate
8 steps
java
@Service public class PaymentGatewayClient { private static final Logger log = LoggerFactory.getLogger(PaymentGatewayClient.class);
Resilient payment calls with Spring Retry
retry
backoff
fault-tolerance
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-spring-cache-annotations-keep-data-fresh-explained-java-5a36/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.