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
public final class Levenshtein { private Levenshtein() { }
Two-row Levenshtein distance in Java
dynamic-programming
edit-distance
space-optimization
Intermediate
8 steps
java
package com.example.lb; import java.util.List; import java.util.concurrent.atomic.AtomicInteger;
A thread-safe round-robin load balancer in Java
concurrency
load-balancing
round-robin
Intermediate
9 steps
python
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status from pydantic import BaseModel, EmailStr from sqlalchemy.orm import Session
Building a signup endpoint in FastAPI
dependency-injection
request-validation
background-tasks
Intermediate
8 steps
rust
use axum::{extract::{Path, State}, http::StatusCode, Json}; use dashmap::DashMap; use serde::Serialize; use std::sync::Arc;
Request coalescing in an Axum handler
caching
concurrency
request-coalescing
Advanced
8 steps
java
public interface GitHubClient { @GetExchange("/users/{username}") GitHubUser getUser(@PathVariable String username);
Declarative HTTP clients in Spring
http-client
declarative-api
proxy
Intermediate
8 steps
php
<?php namespace App\Console\Commands;
Releasing stale document locks in Laravel
artisan-command
transactions
row-locking
Intermediate
6 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.