skip to content

Why does an @Transactional (or any advised) method fail to take effect when called from another method in the same class, and how do you fix it?

level: middleimportance: must knowfreq 65%

answer

  1. this = raw target, not proxy
  2. Internal call skips advice chain
  3. @Transactional/@Cacheable/@Async silently no-op
  4. Fix: extract bean / self-inject / AopContext
  5. AspectJ has no proxy boundary

basics

~20 s

Spring AOP advice runs in the proxy that wraps the bean. An internal call like this.save() goes straight to the real object and never touches the proxy, so the advice (transaction, cache, retry) is skipped. Fix: call through the proxy or use AspectJ.

solid answer

~40 s

Spring AOP is proxy-based: advice only fires on calls that enter the object *through the proxy*. When method A calls method B via `this.B()`, the call is a plain in-object invocation on the raw target — it bypasses the proxy entirely, so B's `@Transactional`, `@Cacheable`, `@Async`, or custom advice never runs. This is the classic 'self-invocation' pitfall. Fixes, roughly in order of cleanliness: (1) refactor B into a separate bean and inject it, so the call crosses a proxy boundary; (2) inject the bean into itself (or an `ObjectProvider`) and call `self.B()`; (3) use `AopContext.currentProxy()` with `exposeProxy=true`; (4) switch to **full AspectJ** load-time or compile-time weaving, which advises the actual method regardless of how it is called and has no proxy boundary at all.

code

java · 17 lines
java
@Service
public class OrderService {

    // Option 2: self-injection — Spring injects the PROXY, not 'this'
    @Autowired
    private OrderService self;

    public void placeOrder() {
        // this.charge();  // BUG: bypasses proxy, @Transactional ignored
        self.charge();     // FIX: goes through the proxy, transaction starts
    }

    @Transactional
    public void charge() {
        // ... runs inside a real transaction now
    }
}

go deeper

for a junior

Aware that internal calls can skip transactions; may not know the proxy reason.

for a middle

Must explain the proxy-boundary root cause and give at least one correct fix.

for a senior

Compares all fixes with trade-offs and connects the issue to the proxy-vs-weaving model.

for a principal

Judges when pervasive self-invocation justifies moving the codebase to AspectJ vs. refactoring bean boundaries.

## The root cause: proxies intercept only boundary crossings Spring AOP works by putting a **proxy** in front of your bean (a JDK dynamic proxy or a CGLIB subclass). The dependency-injection container hands *other* beans the **proxy**, and the proxy is what runs the advice chain before delegating to the real target. The advice chain therefore only executes when a call *enters* the object through that proxy — i.e., an **external** call from another bean. Inside the target object, `this` refers to the **raw, un-proxied instance**. So: ```java @Service class OrderService { public void placeOrder() { // 'this' is the raw target, NOT the proxy charge(); // <-- proxy is bypassed, @Transactional on charge() is ignored } @Transactional public void charge() { /* ... */ } } ``` Calling `placeOrder()` from a controller goes through the proxy, but `placeOrder`'s internal `charge()` call is a direct virtual dispatch on `this` — the proxy never sees it, so **no new transaction is started**. The same failure hits `@Cacheable`, `@Async`, `@Retryable`, `@PreAuthorize`, and any custom aspect. ## Why AspectJ does NOT have this problem Full **AspectJ** does not use proxies. Its **weaver** rewrites the bytecode of the target class itself (at compile time or as the class loads), inlining the advice directly into the method body. There is no separate proxy object and no `this`-vs-proxy distinction — the advice runs no matter who calls the method or how. This is one of the headline reasons teams switch from Spring AOP to AspectJ. ## The fixes (trade-offs) 1. **Extract to another bean (preferred).** Move `charge()` into its own `@Service` and inject it. The call `paymentService.charge()` now crosses a proxy boundary, so advice fires. Cleanest and most testable. 2. **Self-injection.** Inject the bean into itself and call through that reference: ```java @Autowired private OrderService self; // Spring injects the PROXY public void placeOrder() { self.charge(); } ``` Works because `self` is the proxy. Slightly smelly; use `@Lazy` or `ObjectProvider<OrderService>` to avoid circular-init issues. 3. **`AopContext.currentProxy()`.** Enable `@EnableAspectJAutoProxy(exposeProxy = true)`, then `((OrderService) AopContext.currentProxy()).charge();`. Couples code to Spring AOP and is verbose. 4. **Full AspectJ (compile-time or load-time weaving).** Eliminates the problem structurally. Justified when self-invocation is pervasive or you also need richer join points. ## Interview gotchas - The method must also be **public** (proxy rule) and the class/method **non-final** for CGLIB. - A `private` `@Transactional` method never works under Spring AOP even from outside. - People 'fix' it by adding `@Transactional(propagation = REQUIRES_NEW)` on the inner method — that does nothing, because the inner call is never intercepted in the first place.

  • Would switching to AspectJ weaving fix self-invocation, and why?
    Yes. AspectJ weaves advice into the method's own bytecode, so there is no proxy boundary — the advice runs regardless of whether the call is internal or external.
  • A teammate says adding REQUIRES_NEW to the inner method fixes it. Are they right?
    No. The inner call is never intercepted at all, so propagation settings are irrelevant. The interception, not the propagation, is what is missing.

saying these in an interview costs you the question

  • Thinking propagation/isolation settings fix a bypassed self-call
  • Believing 'this' is the proxy inside the bean
  • Assuming making the inner method private helps (it makes it worse)
  • Not knowing AspectJ sidesteps the whole issue

context