skip to content

Why exactly does an internal self-invocation bypass @Transactional, in terms of what reference the call uses?

level: middleimportance: must knowfreq 65%

answer

  1. injected bean = proxy, this = target
  2. advice runs only when entering via proxy
  3. bare method() is this.method()
  4. fixes re-route through a proxy reference
  5. target object doesn't know it's proxied

basics

~10 s

Injected beans are actually proxies. Transaction logic lives in the proxy, not your object. this.method() uses the raw object reference, so it never goes through the proxy and the transaction code is skipped.

solid answer

~40 s

Spring's declarative transactions are applied by a proxy that wraps the target bean. When another component calls the bean, it holds a reference to the proxy, so the transaction advice (begin/commit/rollback) executes before delegating to the real method. Inside the bean, however, `this` refers to the **target object itself**, not the proxy. So `this.method()` — or the bare `method()`, which is implicitly `this.` — invokes the real method directly, completely bypassing the proxy and its advice. The transaction annotation on the inner method is therefore never honored. The root cause is purely about the *reference*: proxy reference → advice runs; `this` reference → advice skipped. This is why the standard fixes all funnel the call back through a proxy reference (self-injection, a separate bean, or AopContext.currentProxy()), or eliminate proxies via AspectJ weaving.

code

java · 18 lines
java
@Service
public class ReportService {

    // 'self' is the PROXY (same bean, injected back into itself).
    @Autowired @Lazy
    private ReportService self;

    public void generate() {
        // BAD: this.compute() -> raw target, @Transactional ignored
        // compute();

        // GOOD: goes through the proxy -> transaction advice runs
        self.compute();
    }

    @Transactional
    public void compute() { /* ... */ }
}

go deeper

for a junior

Enough to say injected bean is a proxy and this.method() skips it.

for a middle

Should clearly articulate proxy-reference vs this-reference and why bare method() == this.method().

for a senior

Should connect the reference explanation directly to why each fix works.

for a principal

Should note it generalizes to all proxy-based Spring AOP and reason about detection/prevention at scale.

## The core idea: it's about the reference Declarative transaction management is **AOP applied via a proxy**. Spring builds a proxy object that has the same type as your bean (JDK dynamic proxy if the bean implements an interface, CGLIB subclass proxy otherwise). The proxy holds a reference to the real **target** object. The container injects the **proxy** wherever the bean is wired. The transactional behavior — starting a transaction through `PlatformTransactionManager`, committing on success, rolling back on a matching exception — lives in an **interceptor** (`TransactionInterceptor`) that the proxy invokes *around* each call. Crucially, that interceptor only fires when the call **enters through the proxy**. ## Two references, two outcomes ``` Other bean ──(holds proxy)──▶ [ proxy ] ──▶ [ target object ] ▲ │ advice runs here │ this.method() └──────────▶ target object (advice SKIPPED) ``` - A call from outside uses the **proxy reference** → interceptor runs → transaction managed. - A call from inside uses **`this`** (the target's own reference) → the call is a plain Java method invocation that never re-enters the proxy → interceptor does **not** run. In Java, an unqualified method call `foo()` inside an instance method is exactly `this.foo()`. `this` is always the concrete target object, never the proxy — the proxy is a *separate object* wrapping it and the target has no idea it is being proxied. That is the whole reason self-invocation is invisible to the aspect. ## Consequences - The inner method's `@Transactional` — including `propagation`, `isolation`, `readOnly`, `timeout`, `rollbackFor` — is entirely ignored. - If the inner method was meant to start `REQUIRES_NEW`, it silently joins the caller's context instead. - No error is raised; the code runs and often *appears* to work until an edge case (partial rollback, isolation) exposes it. ## Why the fixes work All fixes restore an entry *through a proxy* (or remove the proxy model): - **Self-injection** — call through an injected reference to yourself, which is the proxy. - **Separate bean** — the call crosses into another bean via *its* proxy. - **AopContext.currentProxy()** — grabs the proxy for the current invocation (needs `exposeProxy = true`). - **AspectJ weaving** — weaves advice into the bytecode so there is no proxy indirection; every call site, including `this.method()`, is advised. ## Note on scope The deep mechanics of how the proxy/AOP interception is built are a separate topic; here the key exam point is simply: **transaction advice lives on the proxy, `this` is not the proxy, so internal calls skip it.**

  • If the bean implements an interface, does JDK vs CGLIB proxy change the self-invocation behavior?
    No. Both JDK dynamic proxies and CGLIB subclass proxies wrap the target as a separate object. In either case `this` inside the target is not the proxy, so self-invocation bypasses the advice identically.
  • Does making the field-injected 'self' reference create a circular dependency problem?
    It can, since the bean depends on itself. Spring usually resolves it, but adding @Lazy defers resolution and reliably avoids BeanCurrentlyInCreationException in setter/constructor cases.

saying these in an interview costs you the question

  • Saying 'this' points to the proxy
  • Claiming the JVM inlines the call so the annotation is lost (it's about references, not inlining)
  • Believing only CGLIB proxies have this issue
  • Thinking the interceptor runs on every method invocation of the target

context