skip to content

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%

answer

  1. separate bean = cleanest default
  2. self-inject proxy (field/@Lazy, not ctor)
  3. exposeProxy + AopContext.currentProxy()
  4. AspectJ weaving = no proxy, truly fixes it
  5. refactor changes tx boundaries — check propagation

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.

solid answer

~40 s

Four fixes. (1) Refactor: extract the annotated method into a separate collaborator bean and inject it, so the call is external and passes through that bean's proxy — cleanest, most testable, my default. (2) Self-injection: @Autowired the bean into itself (Spring injects the proxy) and call self.method(); quick but a design smell, and constructor self-injection can loop so use field/setter or @Lazy. (3) exposeProxy: set @EnableAspectJAutoProxy(exposeProxy=true) and call ((MyType) AopContext.currentProxy()).method(); works but couples code to Spring AOP and is easy to forget to enable. (4) AspectJ weaving (compile-time or load-time via spring-aspects/@EnableLoadTimeWeaving): advice is woven into the bytecode so there's no proxy and self-invocation is advised correctly — most powerful but heaviest setup. I reach for refactoring first and AspectJ only when internal advice is needed broadly.

code

java · 24 lines
java
// Fix 2: self-injection via @Lazy constructor param (avoids ctor cycle)
@Service
public class OrderService {

    private final OrderService self;

    public OrderService(@Lazy OrderService self) {
        this.self = self; // Spring injects the PROXY lazily
    }

    public void placeOrder(Order o) {
        validate(o);
        self.save(o); // through the proxy -> @Transactional applies
    }

    @Transactional
    public void save(Order o) { /* ... */ }

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

// Fix 3: exposeProxy alternative
// @EnableAspectJAutoProxy(exposeProxy = true) on a @Configuration class, then:
//   ((OrderService) AopContext.currentProxy()).save(o);

go deeper

for a junior

Can name 'move it to another bean' as a fix.

for a middle

Lists 2-3 fixes and knows self-injection uses the proxy.

for a senior

Compares all four fixes with concrete trade-offs and picks refactoring as default.

for a principal

Weighs codebase-wide AspectJ adoption vs localized workarounds, and considers transaction-boundary/propagation impact of refactoring.

## The four fixes, in order of preference ### 1. Extract to a separate bean (preferred) Move the `@Transactional` (or `@Cacheable`, etc.) method into a **different Spring bean** and inject that collaborator. Now the call from the outer bean is an **external** call that goes through the collaborator's proxy, so advice runs. ```java @Service class OrderWriter { @Transactional public void save(Order o){...} } @Service class OrderService { private final OrderWriter writer; OrderService(OrderWriter w){ this.writer = w; } public void placeOrder(Order o){ validate(o); writer.save(o); } } ``` Pros: clean separation of concerns, fully testable, no Spring-AOP coupling. Cons: introduces another class; sometimes feels heavy for a tiny method. **This is the idiomatic answer.** ### 2. Self-injection Inject the bean into itself; Spring provides the **proxy**, and calling through it triggers advice. ```java @Service class OrderService { @Autowired private OrderService self; // the proxy public void placeOrder(Order o){ validate(o); self.save(o); } @Transactional public void save(Order o){...} } ``` Pros: minimal change. Cons: a recognized code smell; **constructor injection of self creates a circular dependency** — use field or setter injection, or `@Lazy` on a constructor parameter. Easy for the next dev to 'clean up' the `self.` and reintroduce the bug. ### 3. exposeProxy + AopContext.currentProxy() Enable proxy exposure and grab the current proxy from a thread-local. ```java @EnableAspectJAutoProxy(exposeProxy = true) ... ((OrderService) AopContext.currentProxy()).save(o); ``` (`@EnableCaching`/`@EnableTransactionManagement` also honor `exposeProxy` semantics via the corresponding auto-proxy config; the flag makes Spring store the proxy in an `AopContext` thread-local for the duration of the call.) Pros: no extra bean, no self-field. Cons: **couples business code to Spring's AOP API**, needs the global flag enabled (throws `IllegalStateException: Cannot find current proxy` if forgotten), and the cast is ugly. ### 4. AspectJ weaving (removes the limitation) Use **real AspectJ** instead of proxies: either **compile-time weaving** (the AspectJ compiler `ajc`) or **load-time weaving** (`@EnableLoadTimeWeaving` + `spring-aspects` + a Java agent / `spring-instrument`). AspectJ **weaves the advice directly into the method's bytecode**, so there is no wrapper and `this.method()` is advised just like any external call — self-invocation works. `@Transactional` and `@Cacheable` can run in AspectJ mode (`mode = AdviceMode.ASPECTJ`). Pros: the only approach that fully solves internal advice, and it can advise `private`/`final` methods too. Cons: build/agent complexity, a JVM agent for LTW, slightly harder debugging, and it's a big hammer for one method. ## Choosing - One-off internal-call bug → **refactor** (or self-inject if extraction is awkward). - You keep hitting self-invocation across the codebase and need internal advice everywhere → **AspectJ**. - Quick localized escape hatch where refactoring is disruptive → **exposeProxy**. ## Gotchas across all fixes - Refactoring changes transaction **boundaries** — make sure propagation still makes sense. - Self-injection and exposeProxy still rely on proxies, so they inherit CGLIB's `final`/`private` limits. - AspectJ LTW must be configured before the target classes load; ordering/agent setup is the usual failure point.

  • Why can constructor-based self-injection fail, and how do you avoid it?
    Building the bean needs a fully built instance of itself, a circular dependency Spring can't resolve eagerly. Use field/setter injection, or annotate the constructor parameter @Lazy so Spring injects a lazy proxy.
  • Which fix also lets you advise private or final methods?
    AspectJ weaving — because advice is woven into the bytecode rather than added via a subclass/wrapper, it isn't limited by CGLIB's inability to override private/final methods.
  • What happens if you call AopContext.currentProxy() without exposeProxy=true?
    It throws IllegalStateException: 'Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true'.'

saying these in an interview costs you the question

  • Recommending exposeProxy as the default rather than refactoring
  • Constructor-injecting the bean into itself without @Lazy
  • Claiming self-injection avoids the proxy (it uses the proxy)
  • Thinking AspectJ is just a config flag with no build/agent cost
  • Forgetting that refactoring can shift transaction boundaries/propagation

context