skip to content

Self-Invocation Bypasses the Proxy

Calling this.method() inside the same bean bypasses the proxy, so @Transactional or @Cacheable silently does nothing. This is the single most-asked Spring gotcha, and interviewers want the fixes as well as the cause.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is self-invocation in Spring AOP, and why can it make @Transactional silently do nothing?

level: juniorimportance: must knowfreq 78%

answer

  1. this.method() hits target, skips proxy
  2. advice lives on proxy, not target
  3. silent: no transaction, no error
  4. fixes: separate bean / self-inject / exposeProxy / AspectJ
  5. affects @Cacheable @Async @Retryable too

basics

~20 s

When a bean calls its own method via this.method(), the call goes straight to the real object, not through Spring's proxy. Because advice like @Transactional lives on the proxy, it is skipped, so no transaction starts.

solid answer

~40 s

Spring applies @Transactional, @Cacheable, @Async etc. through a proxy that wraps your bean. External callers get the proxy and its advice runs. But an internal call such as this.save() (or calling another annotated method in the same class) executes on the raw target object directly — the proxy is never involved — so the surrounding advice is skipped. The annotated method runs with no transaction, no caching, no async, and there is usually no error or warning; the annotation just appears ignored. This bites people when a public method (no annotation) calls a @Transactional method in the same class expecting a transaction. Fixes: move the method to a different bean, self-inject the proxy, use AopContext.currentProxy() with exposeProxy, or switch to AspectJ weaving which doesn't rely on proxies.

code

java · 19 lines
java
@Service
public class OrderService {

    private final OrderRepository repo;
    public OrderService(OrderRepository repo) { this.repo = repo; }

    // Called through the proxy (external caller) -> but NOT annotated
    public void placeOrder(Order o) {
        validate(o);
        save(o); // internal this.save(o): bypasses proxy -> @Transactional ignored!
    }

    @Transactional
    public void save(Order o) {
        repo.persist(o); // runs WITHOUT a transaction when reached via placeOrder
    }

    private void validate(Order o) { /* ... */ }
}

go deeper

for a junior

Must be able to state that an internal this.method() call skips the proxy so @Transactional does nothing, with no error.

for a middle

Explain the proxy-vs-target mechanism and that it affects all proxy-based advice, and name at least one fix.

for a senior

Compare fixes (refactor, self-inject, exposeProxy, AspectJ) and their trade-offs, including propagation implications.

for a principal

Discuss why proxy AOP is caller-side, when AspectJ weaving is justified project-wide, and the maintainability cost of each workaround.

## The mechanism Spring's declarative features like `@Transactional`, `@Cacheable`, `@Async`, and custom `@Aspect` advice are implemented with **AOP proxies**. When a bean needs advice, Spring does not hand other beans the raw object. Instead it wraps it in a **proxy** — either a JDK dynamic proxy (if the bean implements an interface) or a CGLIB subclass proxy (the default for classes). The proxy has the same type/methods as your bean. Every method it exposes first runs the **interceptor chain** (e.g. `TransactionInterceptor` opens a transaction, `CacheInterceptor` checks the cache) and then delegates to the real target object. Key fact: **advice only runs when a call passes through the proxy.** External beans hold a reference to the proxy, so their calls are advised. But inside the target object, `this` refers to the **raw, unwrapped instance** — not the proxy. So any call written as `this.method()` (and `method()` with an implicit `this`) goes straight to the target and **bypasses the entire interceptor chain**. ## The classic symptom ```java @Service public class OrderService { public void placeOrder(Order o) { validate(o); save(o); // this.save(o) -> NO transaction! } @Transactional public void save(Order o) { repo.persist(o); } } ``` Calling `orderService.placeOrder()` from a controller goes through the proxy, but `placeOrder` itself is not transactional. The nested `save(o)` is an internal `this` call, so `TransactionInterceptor` never fires. `save` runs with **no transaction**. There is no exception and no log line — the `@Transactional` is silently ineffective. The same happens with `@Cacheable`: the cache is never consulted or populated for internal calls. ## Why it happens (proxy vs target) Proxy-based AOP is **caller-side** interception: the decision to run advice is made when someone *invokes a method on the proxy reference*. The target has no knowledge that it is proxied and cannot route its own calls back through the wrapper. This is a fundamental limitation of proxy-based AOP, documented explicitly in the Spring reference under 'Understanding AOP Proxies'. ## Fixes 1. **Refactor into a separate bean (preferred).** Move the annotated method to another `@Service`/`@Component` and inject it. The call is now external, so it goes through that bean's proxy. Cleanest and most testable. 2. **Self-injection.** Inject the bean into itself (Spring gives you the proxy) and call through that reference: ```java @Autowired private OrderService self; ... self.save(o); // goes through proxy ``` Works, but is a bit of a smell and needs the field-injected proxy (setter/field, not constructor, to avoid a circular-constructor problem — or use `@Lazy` on a constructor param). 3. **`AopContext.currentProxy()`.** Enable `@EnableAspectJAutoProxy(exposeProxy = true)` (or `exposeProxy=true` on `<aop:config>` / transaction proxy) and call `((OrderService) AopContext.currentProxy()).save(o)`. It ties your code to Spring AOP, so use sparingly. 4. **AspectJ weaving (compile-time or load-time).** With real AspectJ (`@EnableLoadTimeWeaving` + `spring-aspects`, or the AspectJ compiler), advice is woven **into the bytecode of the method itself**, not into a wrapper. Because there is no proxy, self-invocation is advised correctly. Heavier setup, but the only approach that fully removes the limitation. ## Edge cases and gotchas - Applies to **all** proxy-based advice, not just transactions: `@Cacheable`, `@CacheEvict`, `@Async`, `@Retryable`, `@PreAuthorize`, custom aspects. - `private`/`final` methods can't be advised by CGLIB either — separate but related gotcha. - Same-class calls to a **different** annotated method still self-invoke; the problem is `this`, not same-method recursion specifically. - Different `@Transactional` **propagation** (e.g. `REQUIRES_NEW`) is also ignored on self-invocation — a common trap when someone expects a new inner transaction. - Constructor-based self-injection can cause a circular dependency error; use field/setter injection or `@Lazy`. ## When to use which fix Refactor to a separate collaborator for clean designs. Use self-injection or `exposeProxy` for a quick, localized fix. Choose AspectJ only when you genuinely need fine-grained/internal advice project-wide and accept the build/agent complexity.

  • Does the same problem affect @Cacheable and @Async, or only @Transactional?
    It affects all proxy-based advice — @Cacheable, @CacheEvict, @Async, @Retryable, @PreAuthorize, and custom aspects — because they all run through the same proxy that an internal this call skips.
  • Would you get an exception when the transaction is skipped?
    No. That's what makes it dangerous — the annotation is silently ignored, the method just runs non-transactionally with no error or warning.

saying these in an interview costs you the question

  • Thinking @Transactional always works regardless of how the method is called
  • Believing Spring logs an error or warning when advice is skipped
  • Assuming it only affects transactions, not caching/async
  • Confusing 'this' inside the bean with the proxy reference

context

open as a page

You have a self-invocation bug where an internal call to a @Transactional method isn't transactional. What are your fix options and their trade-offs?

level: seniorimportance: must knowfreq 66%

basics

~20 s

Options: move the method to a separate bean (cleanest), self-inject the bean and call through the injected proxy, use AopContext.currentProxy() with exposeProxy=true, or switch to AspectJ weaving. First is preferred; last removes the limitation entirely.

open as a page

Explain the proxy mechanism that causes self-invocation to bypass advice. How do JDK vs CGLIB proxies fit in?

level: middleimportance: should knowfreq 60%

basics

~20 s

Spring wraps your bean in a proxy object that runs advice before delegating to the real target. Other beans get the proxy; but inside the bean, this is the raw target, so its own calls skip the proxy and its advice.

open as a page

How does exposeProxy / AopContext.currentProxy() actually work, and when is it the right tool?

level: seniorimportance: should knowfreq 42%

basics

~10 s

With exposeProxy=true, the proxy stores itself in a thread-local before delegating to the target. Inside the method you call AopContext.currentProxy() to get that proxy and invoke the annotated method through it, so advice runs.

open as a page

Architecturally, when would you move from Spring proxy AOP to AspectJ weaving to solve self-invocation, and what are the costs?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Switch to AspectJ when you need advice to fire on internal calls (and even private/final methods) across the codebase, not just one spot. Costs: compile-time or load-time weaving setup, a JVM agent for LTW, harder debugging, and broader coupling to AspectJ.

open as a page