Spring’s caching abstraction annotates methods to store their result under a cache and key. It is convenient but hides an important boundary: the cache sits around a proxy, so how you call the method and how you invalidate decide whether it works.
Before you start
You should be comfortable with beans, proxies and a cache provider. This article covers caching annotations and their pitfalls.
Step-by-step walkthrough
Step 1: Annotate and choose a key
@Cacheable("orders") stores the result keyed by the method arguments, and @Cacheable(cacheNames = "orders", key = "#id") names the key explicitly. A precise key avoids collisions between different arguments, and the cache name groups related entries.
Step 2: Configure TTL and eviction
The annotation stores indefinitely unless the provider has a TTL, so configure a time-to-live or max size in the provider. Evict or update on writes with @CacheEvict or @CachePut, so a changed record is not served stale from the cache.
Step 3: Mind the proxy boundary
The cache is applied by a proxy around the bean, so a call from within the same bean — this.method() — bypasses the proxy and the cache. External calls are cached; internal ones are not. This is a common “why is my cache not working” cause.
Worked scenario
The service caches reads and evicts on update.
@Service
public class UserService {
@Cacheable(cacheNames = "users", key = "#id")
public User get(String id) { return repo.findById(id).orElseThrow(); }
@CacheEvict(cacheNames = "users", key = "#user.id")
public void update(User user) { repo.save(user); }
}Walk through the example
get stores the user under the id key, so repeated reads hit the cache. update evicts that key, so the next read fetches the fresh value instead of the stale one. The calls must come from outside the bean for the proxy to apply, which is why the methods are public and called through the injected bean.
Common mistake
Caching mutable data without eviction, so updates are invisible until the TTL expires, or calling a cached method internally and expecting it to be cached. Another is no TTL, so entries persist and memory grows.
Verify the behavior
Call the cached method twice and confirm the repository is hit once. Update the entity and confirm the next read fetches fresh data. Call the method internally and confirm the cache is bypassed, demonstrating the proxy boundary.
Interview exercise
Why is a cache annotation bypassed on an internal method call?
Answer and reasoning
Because Spring applies caching through a proxy that wraps the bean. An external caller goes through the proxy, which checks the cache; a call from within the same bean uses this and never crosses the proxy, so the annotation is not applied. Moving the cached method to another bean restores caching.
Continue learning
Compare proxying in Transaction proxies and cache design in Cache aside. Read the Spring caching documentation and try the Spring Boot interview questions.