The self-invocation problem with @Cacheable. — Cracked Java
// Spring Framework & Spring Boot · Caching
SeniorTrick

The self-invocation problem with @Cacheable.

@Cacheable works through an AOP proxy, so when one method of a bean calls another @Cacheable method on this, the call never leaves the object — it bypasses the proxy and the cache annotation does nothing. It's the exact same failure mode as @Transactional, for the exact same reason.

Why it breaks

Spring doesn't modify your class bytecode. It wraps your bean in a proxy (CGLIB subclass or JDK dynamic proxy) and injects the proxy wherever the bean is autowired. The caching logic — key computation, cache lookup, store — lives in the proxy's method interception, not in your method body.

A call from outside goes caller -> proxy -> your method, so the proxy runs. But an internal call uses the implicit this reference, which points at the raw, unwrapped instance, not the proxy. So caller -> proxy -> methodA -> this.methodB() skips the proxy on the second hop, and methodB's @Cacheable is silently ignored.

@Service
class BookService {
  public Book report(long id) {
    return findById(id);          // self-call: NOT cached
  }
  @Cacheable("books")
  public Book findById(long id) { /* expensive */ }
}

report runs the expensive lookup every time, even though findById is annotated.

Fixes

1. Split into two beans (preferred). Move the cached method to a separate bean and inject it; now the call crosses a proxy boundary.

@Service class BookFacade {
  private final BookLookup lookup;   // injected proxy
  public Book report(long id) { return lookup.findById(id); }
}

2. Self-injection. Inject the bean into itself and call through the proxy reference:

@Autowired private BookService self;
public Book report(long id) { return self.findById(id); }

3. AopContext.currentProxy(). Requires @EnableCaching(exposeProxy = true), then ((BookService) AopContext.currentProxy()).findById(id). Works but couples code to Spring AOP — least clean.

Mark your status