Ch. 8 · Spring Boot

Spring Boot Caching Abstraction

Cache method results with @Cacheable, choose keys and TTLs, and evict on writes without the self-invocation trap.

~2 min readadvancedupdated Oct 5, 2026

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); }
}
java

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.

More in Spring Boot

read ✓Spring Boot · hard

Spring @Async and Executor Configuration

Run methods asynchronously with @Async, configure a bounded executor, and handle exceptions and the proxy boundary.

~2 min readread →
read ✓Spring Boot · hard

Spring Declarative HTTP Clients

Define outbound HTTP as an annotated interface with @HttpExchange, create the proxy, and configure timeouts and errors.

~2 min readread →
read ✓Spring Boot · mid

Spring Boot Problem Details for APIs

Return consistent RFC 7807 error responses, map exceptions centrally, and avoid leaking internals to clients.

~2 min readread →
esc