skip to content

Your @Transactional inner method is being ignored because of self-invocation. Walk through the available fixes and their tradeoffs.

level: seniorimportance: must knowfreq 60%

answer

  1. separate bean = cleanest
  2. self-inject + @Lazy
  3. AopContext.currentProxy + exposeProxy
  4. AspectJ mode = weaves bytecode, no proxy
  5. public/final does NOT fix it

basics

~20 s

Three options: extract the inner method into a separate bean and call it (cleanest); inject the bean into itself and call through that proxy reference; or switch transactions to AspectJ weaving so all calls, including this.method(), are advised.

solid answer

~50 s

First, the preferred fix is **redesign**: move the inner method to its own bean and inject it, so the call crosses a real proxy boundary and the advice runs. This usually also improves separation of concerns. If splitting is awkward, use **self-injection** — inject the bean into itself (often with `@Lazy` to sidestep the self-referential dependency) and call `self.method()`, which is the proxy. A variant is `AopContext.currentProxy()` combined with `@EnableAspectJAutoProxy(exposeProxy = true)`, but that couples code to Spring AOP. The heavyweight fix is **AspectJ weaving** via `@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)` with load-time or compile-time weaving; it weaves advice into bytecode so even self-invocation is intercepted, removing the limitation globally — at the cost of a weaver agent / build step. I default to bean extraction; reach for self-injection for one-offs, and AspectJ only when self-calls are pervasive.

code

java · 20 lines
java
// Recommended fix: extract inner tx logic into its own bean
@Service
public class OrderService {
    private final AuditService auditService;
    OrderService(AuditService auditService) { this.auditService = auditService; }

    @Transactional
    public void placeOrder() {
        auditService.record(); // through AuditService's proxy -> REQUIRES_NEW honored
    }
}

@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record() { /* independent transaction */ }
}

// Enable AspectJ mode instead, if you want self-calls advised globally:
// @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)

go deeper

for a junior

May know only 'move it to another bean'; that's acceptable at this level.

for a middle

Should list at least two fixes and pick the separate-bean approach as cleanest.

for a senior

Should present all fixes with tradeoffs and justify a default choice.

for a principal

Should weigh AspectJ's global impact vs targeted fixes, mention the @Lazy circular-ref nuance, and consider team/build implications.

## The problem recap A `this.method()` call bypasses the transactional proxy, so the inner `@Transactional` is ignored. Every fix either routes the call through a proxy again, or eliminates the proxy model. ## Fix 1 — Extract into a separate bean (recommended) Move the inner logic into its own Spring bean and inject it. The call now crosses into a different bean **through that bean's proxy**, so the advice runs normally. ```java @Service public class OrderService { private final AuditService auditService; OrderService(AuditService auditService) { this.auditService = auditService; } @Transactional public void placeOrder() { auditService.record(); // crosses AuditService's proxy -> REQUIRES_NEW honored } } @Service public class AuditService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void record() { /* independent tx */ } } ``` **Pros:** cleanest, no AOP tricks, better cohesion, testable. **Cons:** a new class; overkill if the method is tiny and genuinely belongs where it is. ## Fix 2 — Self-injection Inject the bean into itself and call through that reference (the proxy). ```java @Service public class OrderService { @Autowired @Lazy private OrderService self; @Transactional public void placeOrder() { self.audit(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void audit() { /* new tx works */ } } ``` `@Lazy` avoids `BeanCurrentlyInCreationException` from the circular self-dependency (Spring can usually handle field self-injection, but `@Lazy` makes it robust and works with constructor injection). **Pros:** keeps code in one class. **Cons:** looks odd, is a known code smell, and hides the proxy indirection. ## Fix 3 — AopContext.currentProxy() ```java ((OrderService) AopContext.currentProxy()).audit(); ``` Requires `@EnableAspectJAutoProxy(exposeProxy = true)` (or `<aop:aspectj-autoproxy expose-proxy="true"/>`). **Pros:** no extra field. **Cons:** hard-couples business code to Spring AOP internals; least readable; easy to forget the expose-proxy flag (throws IllegalStateException otherwise). ## Fix 4 — AspectJ weaving Switch the whole transaction infrastructure from proxy mode to AspectJ mode: ```java @Configuration @EnableTransactionManagement(mode = AdviceMode.ASPECTJ) public class TxConfig {} ``` plus `spring-aspects` on the classpath and either **compile-time weaving** (AspectJ compiler) or **load-time weaving** (`-javaagent:spring-instrument.jar` / `@EnableLoadTimeWeaving`). AspectJ weaves the transaction advice **directly into the class bytecode**, so there is no separate proxy object — every call site, including `this.method()`, `private`, and `final` methods, is advised. **Pros:** removes the limitation project-wide; also fixes the private/final-method restriction. **Cons:** significant setup (weaver agent or build integration), a steeper mental model, and it is global rather than targeted. ## Decision guide - **Default:** Fix 1 (separate bean) — best design. - **One-off / can't split:** Fix 2 (self-injection with `@Lazy`). - **Avoid** Fix 3 unless you specifically want no extra field and accept the coupling. - **Fix 4** only when self-invocation is widespread or you need advice on non-public methods. ## Anti-'fix' that does NOT work Making the inner method `public`, or splitting into two `public` methods in the same class, does **not** help — visibility is irrelevant; it is still a `this` call. Nesting the `@Transactional` annotations differently also does nothing.

  • Why add @Lazy on the self-injected field?
    The bean depends on itself, a circular reference. @Lazy injects a lazy-resolving proxy so the dependency is satisfied after the bean is fully constructed, avoiding BeanCurrentlyInCreationException — especially important with constructor injection.
  • What does AspectJ mode fix that the proxy fixes cannot?
    Because it weaves advice into the bytecode rather than wrapping the object, it also advises self-invocations, and even private/final/protected methods — none of which proxy-based AOP can intercept.
  • Does calling AopContext.currentProxy() without exposeProxy=true work?
    No. It throws IllegalStateException: 'Cannot find current proxy: Set 'exposeProxy' property on Advised to make it available'. You must enable exposeProxy.

saying these in an interview costs you the question

  • Claiming making the method public fixes self-invocation
  • Saying AspectJ mode still uses a proxy per call
  • Forgetting exposeProxy=true is required for AopContext.currentProxy()
  • Recommending self-injection as the default over bean extraction without noting it's a smell

context