skip to content

A developer annotates a helper method with REQUIRES_NEW and calls it from another method in the same class, but no separate transaction is created. Why, and how do you fix it?

level: seniorimportance: must knowfreq 63%

answer

  1. @Transactional = proxy (CGLIB/JDK)
  2. this.method() bypasses proxy
  3. Fix: separate bean / self-inject / AopContext / AspectJ
  4. Also breaks on private & final methods
  5. General AOP self-invocation limitation

basics

~20 s

Spring applies @Transactional through a proxy that wraps the bean. An internal call (this.method()) skips the proxy, so the transaction advice never runs. Fix it by calling through an injected bean reference so the call goes through the proxy.

solid answer

~50 s

@Transactional is implemented with Spring AOP: at runtime the bean is wrapped in a proxy (JDK dynamic proxy or CGLIB subclass) that opens/commits transactions around annotated methods. A **self-invocation** — one method in the class calling another via the implicit `this` — goes directly to the target object and never passes through the proxy, so the REQUIRES_NEW advice is skipped and the call just runs in the caller's existing transaction (or none). Fixes: (1) move the REQUIRES_NEW method into a **separate bean** and inject it — the cleanest option; (2) inject the bean into **itself** (self-injection) and call through that reference; (3) obtain the proxy via `AopContext.currentProxy()` with `@EnableAspectJAutoProxy(exposeProxy = true)`; or (4) use AspectJ compile/load-time weaving, which weaves the advice into the bytecode so even self-calls are advised. Option 1 is idiomatic; 3 is discouraged.

code

java · 24 lines
java
// BROKEN: self-invocation bypasses the proxy
@Service
class ReportService {
    @Transactional
    public void run() {
        logAttempt("start"); // this.logAttempt -> NO new tx!
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAttempt(String m) { /* never gets its own tx here */ }
}

// FIX 1 (preferred): separate bean, injected -> call hits the proxy
@Service
class ReportService {
    private final AuditLog audit;
    ReportService(AuditLog audit) { this.audit = audit; }
    @Transactional
    public void run() { audit.logAttempt("start"); } // new tx works
}
@Service
class AuditLog {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAttempt(String m) { /* independent tx */ }
}

go deeper

for a junior

May not know the proxy mechanism; can note 'the annotation didn't take effect'.

for a middle

Should identify self-invocation and suggest calling through another bean.

for a senior

Should explain proxy vs target, list multiple fixes, and note it affects all AOP advice.

for a principal

Should weigh separate-bean vs AspectJ weaving trade-offs and design boundaries to avoid the trap.

## Why it happens: proxy-based AOP Spring doesn't rewrite your class. Instead, when a bean has `@Transactional` (or other advice), Spring registers a **proxy** around it: - **JDK dynamic proxy** if the bean implements an interface (proxies the interface), or - **CGLIB** subclass proxy otherwise (Spring Boot defaults to CGLIB via `proxyTargetClass=true`). Callers get a reference to the **proxy**, not the raw object. The proxy's method interception is where `TransactionInterceptor` runs: it consults the `PlatformTransactionManager`, applies the propagation (here REQUIRES_NEW → suspend + begin new), invokes the real method, then commits/rolls back. ## The self-invocation problem Inside the target object, a call like: ```java public void outer() { this.doInNewTx(); } @Transactional(propagation = REQUIRES_NEW) public void doInNewTx() { ... } ``` The `this.doInNewTx()` call is a **direct virtual method call on the target instance**. It does **not** go back out through the proxy, so `TransactionInterceptor` is never invoked for `doInNewTx`. Result: no new transaction — the code just runs in whatever transactional context `outer()` already has. This silently defeats REQUIRES_NEW and is one of the most common Spring transaction bugs. The same problem affects **any** propagation, `@Async`, `@Cacheable`, etc., and applies to **private** methods too (proxies can't advise private/final methods at all — CGLIB can't override `final`). ## Fixes ### 1. Separate bean (preferred) Move `doInNewTx` into another `@Service`/`@Component` and inject it. The call `otherBean.doInNewTx()` goes through that bean's proxy. ### 2. Self-injection Inject the bean into itself and call through the injected (proxied) reference: ```java @Autowired private MyService self; public void outer() { self.doInNewTx(); } ``` Spring resolves `self` to the proxy. Works, but slightly obscure; guard against circular-init issues (constructor injection of self can fail — use field/setter or `@Lazy`). ### 3. AopContext.currentProxy() Enable `@EnableAspectJAutoProxy(exposeProxy = true)` (or `proxyTargetClass`+exposeProxy), then: ```java ((MyService) AopContext.currentProxy()).doInNewTx(); ``` Functional but couples code to AOP internals — generally discouraged. ### 4. AspectJ weaving (load-time/compile-time) With `mode = AdviceMode.ASPECTJ` and the AspectJ weaver, the transactional advice is woven directly into the bytecode, so **even self-calls and private methods are advised**. Heavier setup; used when proxy limitations are unacceptable. ## Diagnosing it - Turn on `logging.level.org.springframework.transaction=TRACE` / `org.springframework.orm.jpa=DEBUG` and watch for whether a new transaction is begun. - Check `TransactionSynchronizationManager.getCurrentTransactionName()` inside the method. - If you never see a second connection acquired, the advice didn't run. ## Broader lesson This is not specific to REQUIRES_NEW — it's the general **proxy self-invocation limitation** of Spring AOP. Design so that transactional boundaries sit at the **entry** of a bean's public methods called from *other* beans, not from sibling methods of the same class.

  • Does making the REQUIRES_NEW method public but still calling it via this fix the problem?
    No. Visibility isn't the issue — the call still goes through this directly to the target, bypassing the proxy. It must be invoked through the proxied bean reference (separate bean, self-injection, AopContext, or use AspectJ weaving).
  • Why can't a CGLIB proxy advise a final or private method?
    CGLIB works by subclassing the target and overriding methods. It cannot override final methods, and private methods aren't virtual/overridable, so no interception is possible. Such methods silently run without transactional advice.
  • Which approach makes even self-invocations transactional?
    AspectJ weaving (compile-time or load-time, mode=ASPECTJ). It weaves advice into the bytecode of the class itself rather than relying on a wrapping proxy, so internal calls are advised too.

saying these in an interview costs you the question

  • Thinking marking the method public fixes self-invocation
  • Believing Spring rewrites the class so any internal call is advised
  • Assuming private/final methods can be transactional under proxies
  • Suggesting only that the annotation was 'forgotten' rather than the proxy mechanism

context