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?
answer
- separate bean = cleanest default
- self-inject proxy (field/@Lazy, not ctor)
- exposeProxy + AopContext.currentProxy()
- AspectJ weaving = no proxy, truly fixes it
- refactor changes tx boundaries — check propagation
basics
~20 sOptions: 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 sFour 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// 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
Can name 'move it to another bean' as a fix.
Lists 2-3 fixes and knows self-injection uses the proxy.
Compares all four fixes with concrete trade-offs and picks refactoring as default.
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