If a query returns the same result thousands of times between writes, you are paying for the database round trip every time. Spring's cache abstraction lets you memoize method results with annotations, keeping your query code untouched.
Turn caching on
Enable caching and register a cache manager. For local caches, Caffeine is a strong default with good eviction policies.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("products");
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10)));
return manager;
}
}Cache reads, evict on writes
@Cacheable stores the return value keyed by the method arguments. @CacheEvict
clears entries when the underlying data changes so readers never see stale rows.
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
return repository.findById(id).orElseThrow();
}
@CacheEvict(value = "products", key = "#product.id")
public Product update(Product product) {
return repository.save(product);
}
}Avoid the stampede
When a hot key expires, many threads can hit the database at once. Set
sync = true so only one thread recomputes the value while others wait.
@Cacheable(value = "products", key = "#id", sync = true)
public Product findById(Long id) {
return repository.findById(id).orElseThrow();
}Takeaways
- Cache expensive, read-heavy queries — not everything.
- Evict on writes to keep the cache consistent with the database.
- Use
sync = trueto prevent cache stampedes on hot keys.
Start local with Caffeine; move to a distributed cache like Redis once you run more than one instance.