A @Cacheable method's cache is never hit when called from within the same service. Diagnose and give the fix you'd ship.
answer
- CacheInterceptor lives on the proxy
- this.method() = cold cache every call
- check @EnableCaching + CacheManager + key stability
- fix: separate bean > self-inject > AspectJ > AopContext
- test: second call must not recompute
basics
~20 sThe internal call bypasses the caching proxy, so the CacheInterceptor never runs — every call recomputes. Fix by calling the @Cacheable method through the proxy: put it in a separate bean, self-inject the bean, or use AspectJ weaving. Restructuring into a separate bean is the cleanest.
solid answer
~50 s@Cacheable is proxy-based advice (CacheInterceptor). When a method in the same class calls the cached method via this.method(), it hits the raw target, so the cache lookup/store never happens — the method executes fully every time. Diagnosis: verify @EnableCaching is present, a CacheManager bean exists, the key isn't varying unexpectedly, and — most commonly — that the call isn't a self-invocation. The fix I'd ship depends on design: extract the cached method into a dedicated bean so callers cross a proxy boundary (cleanest, testable); or self-inject the bean and call through the injected proxy reference; or, if I control the weaving setup, switch caching to AspectJ mode which intercepts internal calls too. I'd avoid AopContext.currentProxy() in production code as it couples the code to the proxy mechanism. I'd also add a test asserting the second call doesn't re-execute.
code
java · 25 lines// PROBLEM: internal call never caches
@Service
public class PricingService {
public BigDecimal quote(long id) {
return computeQuote(id); // this.computeQuote -> proxy bypassed -> NOT cached
}
@Cacheable("quotes")
public BigDecimal computeQuote(long id) { /* expensive */ }
}
// FIX: extract the cached method into its own bean
@Service
public class QuoteCache {
@Cacheable("quotes")
public BigDecimal computeQuote(long id) { /* expensive */ }
}
@Service
public class PricingService2 {
private final QuoteCache quotes;
PricingService2(QuoteCache quotes) { this.quotes = quotes; }
public BigDecimal quote(long id) {
return quotes.computeQuote(id); // crosses proxy boundary -> cached
}
}go deeper
Should recognize the cache isn't being consulted and suspect the call path.
Should name self-invocation and the separate-bean fix.
Should run the full diagnostic (key stability, @EnableCaching, CacheManager) and justify the chosen fix plus a test.
Should weigh AspectJ weaving vs restructuring at scale and set team conventions to avoid AopContext hacks.
## Why the cache is cold `@Cacheable` is implemented by the **`CacheInterceptor`** advice inside a Spring AOP proxy (enabled via `@EnableCaching`). On a call through the proxy: 1. Compute the cache key (default `SimpleKeyGenerator` from method args, or a SpEL `key`). 2. Look up the key in the target `Cache` (resolved via the `CacheManager`). 3. **Hit** → return cached value, skip the method body. **Miss** → invoke the method, store the result, return it. All of this lives in the **proxy**. An internal `this.getData(id)` call goes directly to the target object, skipping the interceptor — so step 1–3 never run and the method body executes **every time**, populating nothing. This is the exact same **self-invocation** limitation as `@Transactional` and `@Async`. ## Full diagnostic checklist 1. **Self-invocation** — is the cached method invoked via `this.` from the same bean? (Most common cause.) 2. **@EnableCaching present?** Without it, all cache annotations are inert. 3. **CacheManager bean?** No manager → no caching. 4. **Key stability** — a SpEL `key` referencing mutable/identity-based fields, or including a timestamp, produces a fresh key every call, so it always misses. 5. **`condition` / `unless`** — `unless = "#result == null"` etc. may prevent storing. 6. **`sync = false`** with concurrent misses can recompute; not a correctness bug though. 7. **Different bean instance** — calling on a `new` (non-managed) instance. ## Fixes, ranked ### 1. Extract to a separate bean (preferred) Move the `@Cacheable` method into its own `@Service`; inject it. Calls now cross a proxy boundary and are intercepted. Bonus: clearer responsibility, easily unit-testable. ### 2. Self-injection ```java @Autowired private MyService self; ... self.getData(id); ``` The injected reference is the proxy, so the interceptor runs. Works but is a known smell. ### 3. AspectJ weaving `@EnableCaching(mode = AdviceMode.ASPECTJ)` with LTW/CTW weaves the caching advice into the class, so even `this.getData(id)` is cached. Fixes self-invocation universally but requires the weaver agent. ### 4. AopContext.currentProxy() `((MyService) AopContext.currentProxy()).getData(id)` with `exposeProxy=true`. Works but couples to AOP internals — avoid in clean code. ## Ship-quality answer Extract to a separate bean (or self-inject if extraction is disproportionate), and add a test that calls twice and verifies the underlying computation ran once (e.g. Mockito `verify(..., times(1))` on the collaborator, or a counter). Confirm `@EnableCaching` and a `CacheManager` are present.
- Besides self-invocation, name two reasons a @Cacheable method might always miss.An unstable cache key — e.g. a SpEL key referencing a value that changes each call, or the default key over arguments that include a mutable/identity object — so every call computes a new key. Also: @EnableCaching or the CacheManager bean is missing, or an unless/condition expression prevents storing the result.
- Why avoid AopContext.currentProxy() in production code?It hard-couples business code to Spring's proxy mechanism, requires exposeProxy=true globally, breaks if weaving/proxy strategy changes, and is hard to unit-test. Extracting the cached method into a separate collaborator bean achieves the same effect with cleaner, testable design.
saying these in an interview costs you the question
- Blaming the CacheManager or cache provider before checking self-invocation
- Suggesting to make the cached method static
- Assuming @Cacheable caches regardless of call path