How to solve the self-invocation problem
The self-invocation problem occurs because internal calls using this.method() bypass Spring's runtime AOP dynamic proxy. As a result, declarative annotations such as @Transactio...
🟢 Junior Level
The self-invocation problem occurs because internal calls using this.method() bypass Spring’s runtime AOP dynamic proxy. As a result, declarative annotations such as @Transactional, @Async, @Cacheable, and @PreAuthorize are completely ignored.
The Recommended Solution: Extract Method to a Separate Service
The cleanest, most idiomatic solution is to adhere to the Single Responsibility Principle (SRP) by extracting the annotated method into a dedicated collaborator bean.
// ❌ BEFORE: Self-invocation within the same class (Proxy is bypassed; NO transaction!)
@Service
public class OrderService {
public void processOrder(Long orderId) {
// Internal call via 'this' bypasses the Spring AOP proxy!
saveOrder(orderId);
}
@Transactional
public void saveOrder(Long orderId) {
// Runs WITHOUT a transaction!
}
}
// ✅ AFTER: Two distinct Spring beans (Call routes through the proxy; transaction works!)
@Service
public class OrderService {
private final OrderDatabaseService orderDatabaseService;
public OrderService(OrderDatabaseService orderDatabaseService) {
this.orderDatabaseService = orderDatabaseService;
}
public void processOrder(Long orderId) {
// External call through the injected dynamic proxy of OrderDatabaseService!
orderDatabaseService.saveOrder(orderId);
}
}
@Service
public class OrderDatabaseService {
@Transactional
public void saveOrder(Long orderId) {
// Executes cleanly inside an active database transaction!
}
}
🟡 Middle Level
Comparison of the 5 Solutions to Self-Invocation
| Approach | Working Principle | Pros | Cons | Recommendation |
|---|---|---|---|---|
| 1. Separate Service | Splits responsibilities across beans (SRP) | Clean architecture, simple unit testing with Mockito | Introduces an extra class | Recommended (Best Practice) |
2. TransactionTemplate |
Programmatic transaction management | No proxy dependence, isolates slow network I/O | Verbose boilerplate callback code | Recommended for high-throughput I/O |
3. Self-Injection (@Lazy) |
Injects dynamic proxy of itself | No new classes needed; keeps related methods together | Creates a circular dependency (resolved lazily) | Acceptable for legacy refactoring |
4. ObjectProvider<Self> |
Retrieves proxy from ApplicationContext on-demand |
Type-safe, explicit runtime resolution | Couples class to Spring API | Modern alternative to @Lazy |
5. AopContext.currentProxy() |
Retrieves proxy from ThreadLocal context |
Quick one-line fix without extra injections | Fragile, requires global exposeProxy, breaks unit tests |
❌ Anti-pattern (Avoid) |
🔴 Senior Level
Solution 2: Programmatic Transactions with TransactionTemplate
Using TransactionTemplate eliminates proxy dependencies entirely. Furthermore, it prevents a major production anti-pattern: holding an idle database connection open during slow external network calls.
package com.example.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@Service
public class InvoiceProcessingService {
private final TransactionTemplate transactionTemplate;
private final PaymentGatewayClient paymentGatewayClient;
private final InvoiceRepository invoiceRepository;
public InvoiceProcessingService(TransactionTemplate transactionTemplate,
PaymentGatewayClient paymentGatewayClient,
InvoiceRepository invoiceRepository) {
this.transactionTemplate = transactionTemplate;
this.paymentGatewayClient = paymentGatewayClient;
this.invoiceRepository = invoiceRepository;
}
public void processInvoice(Long invoiceId) {
// 1. Slow external HTTP REST call executed OUTSIDE of any database transaction!
PaymentConfirmation confirmation = paymentGatewayClient.charge(invoiceId);
// 2. Short, tightly bounded database transaction executed programmatically:
transactionTemplate.executeWithoutResult(status -> {
Invoice invoice = invoiceRepository.findById(invoiceId).orElseThrow();
invoice.markAsPaid(confirmation.transactionId());
invoiceRepository.save(invoice);
});
// DB connection is returned to HikariCP immediately after the lambda exits!
}
}
Solution 3: Self-Injection via Constructor with @Lazy
When refactoring large legacy classes where extracting new beans would require sweeping changes, you can inject a proxy reference to the class itself using @Lazy:
package com.example.service;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserService self; // Reference to the dynamic proxy of THIS bean!
public UserService(@Lazy UserService self) {
this.self = self;
}
public void registerUser(UserDto dto) {
// Invoking via 'self' routes through the proxy, triggering TransactionInterceptor!
self.saveUserInNewTransaction(dto);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveUserInNewTransaction(UserDto dto) {
// Executes cleanly inside an isolated, independent new physical transaction
}
}
How @Lazy Constructor Injection Works Under the Hood:
- Without
@Lazy, attempting to injectUserServiceintoUserServicecauses Spring to detect an unresolvable cycle and crash withBeanCurrentlyInCreationException. - When
@Lazyis present on the constructor parameter, Spring generates a synthetic CGLIB proxy placeholder forselfand completes constructor instantiation immediately. - When
self.saveUserInNewTransaction()is first called, the synthetic proxy delegates to theBeanFactoryto fetch the fully initialized singleton proxy forUserService.
Solution 4: On-Demand Lookup via ObjectProvider<Self>
Spring 5+ provides ObjectProvider<T>, which acts as a type-safe, on-demand factory:
@Service
public class OrderService {
private final ObjectProvider<OrderService> selfProvider;
public OrderService(ObjectProvider<OrderService> selfProvider) {
this.selfProvider = selfProvider;
}
public void executeBatch() {
// Explicitly resolves the proxied instance at runtime
selfProvider.getObject().executeTransactionalItem();
}
@Transactional
public void executeTransactionalItem() { ... }
}
Why AopContext.currentProxy() is a Dangerous Anti-Pattern
Legacy Spring projects often used AopContext.currentProxy():
// Requires: @EnableAspectJAutoProxy(exposeProxy = true)
public void outerMethod() {
// ❌ ANTI-PATTERN: Tight coupling and fragile runtime behavior
((OrderService) AopContext.currentProxy()).innerTransactionalMethod();
}
Why This Should Never Be Used in Production:
- Silent Runtime Crashes: It requires
@EnableAspectJAutoProxy(exposeProxy = true). If another developer reconfigures the application or runs in an environment where this flag is missing, the code throwsIllegalStateException: Cannot find current proxyat runtime. - Global Performance Penalty: Setting
exposeProxy = trueforces every single dynamic proxy across the entire Spring context to publish its reference to aThreadLocalon every method invocation, wasting CPU cycles and memory. - Breaks Unit Testing: Plain JUnit tests (
new OrderService().outerMethod()) immediately crash becauseAopContextis uninitialized outside of a live Spring container.
4 Tricky Questions
1. Which solution to self-invocation is considered the architectural gold standard and why?
Answer: Extracting the method into a separate collaborator service bean. This adheres to the Single Responsibility Principle (SRP): it separates orchestration/business flow logic from persistence and transactional boundary logic. Furthermore:
- It eliminates circular dependencies (unlike self-injection).
- It relies on standard Spring dependency injection without proprietary tricks (
@Lazyself-injection orAopContext). - It enables clean, isolated unit testing: each service can be instantiated and tested in plain JUnit tests with standard Mockito mocks (
@Mock).
2. How does TransactionTemplate help avoid connection pool exhaustion when calling external HTTP APIs?
Answer:
When using declarative @Transactional, the database connection is acquired at the start of the method and held open until the method exits. If the method makes an external HTTP request taking 2 seconds, the physical database connection sits completely idle in memory for those 2 seconds, quickly exhausting the HikariCP connection pool under moderate traffic.
With TransactionTemplate, the developer executes the external HTTP call first outside of any transaction. Once the HTTP response is received, the developer opens a localized transaction block via transactionTemplate.execute(...) strictly for the database updates. The connection is borrowed, updated, committed, and returned to the pool within milliseconds.
3. How does @Lazy resolve the circular dependency when injecting a bean into its own constructor?
Answer:
Normally, constructor injection circularities fail immediately because Spring cannot finish invoking the constructor of BeanA without first having an instance of BeanA to pass into it (BeanCurrentlyInCreationException).
When @Lazy is placed on the constructor parameter (public MyService(@Lazy MyService self)), Spring skips eager bean resolution. Instead, it generates a synthetic dynamic CGLIB proxy placeholder and passes that stub into the constructor. The constructor finishes cleanly, and the bean is created. Only when a method is actually invoked on self does the stub dynamically query the BeanFactory to fetch the real, fully initialized singleton proxy.
4. Why is AopContext.currentProxy() considered a harmful anti-pattern in modern microservices?
Answer:
- Configuration Fragility: It depends on the global property
exposeProxy = trueon@EnableAspectJAutoProxy. If omitted, the method fails at runtime withIllegalStateException. - Performance Overhead: Enabling
exposeProxycauses Spring’s AOP interceptors to write to aThreadLocalon every proxy call across the entire application, adding CPU and garbage collection overhead. - Loss of Testability: It couples application code to static Spring framework infrastructure. Writing a lightweight unit test using
new Service(...)throws an exception unless a full Spring test runner (@SpringBootTest) is loaded.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
Self-invocation bypasses Spring AOP runtime proxies because calls via
thisexecute directly on the target instance’s vtable without passing through theMethodInterceptorchain. The 4 ways to solve it:
- Extract Method to a Separate Bean (Best Practice): Refactor the transactional logic into a new collaborator service (clean SRP, easily testable).
TransactionTemplate: Manage transactions programmatically with lambda callbacks (bypasses proxies entirely; critical for isolating transactions from slow HTTP calls).- Self-Injection (
@LazyConstructor /ObjectProvider): Inject the bean’s own proxy reference to callself.method()(useful for legacy refactoring).- AspectJ Bytecode Weaving: Use compile-time or load-time weaving instead of dynamic proxies.
AopContext.currentProxy()is a legacy anti-pattern that should be avoided.
Solutions Summary Table
| Solution | Best Used When… | Trade-off |
|---|---|---|
| Separate Service | Designing greenfield services or refactoring | Adds an extra class file |
TransactionTemplate |
Transactions wrap database updates around slow HTTP/IO | Requires lambda callback boilerplate |
@Lazy Self-Injection |
Refactoring large legacy classes without breaking changes | Introduces a lazy circular dependency |
ObjectProvider<Self> |
Dynamic on-demand lookup needed | Couples code to ObjectProvider API |
Red Flags (DO NOT Say)
- ❌ “The best way to fix self-invocation is to call
AopContext.currentProxy().” (It is an obsolete anti-pattern with global performance penalties and testing issues). - ❌ “You can inject yourself via standard constructor injection without
@Lazy.” (This results in a fatalBeanCurrentlyInCreationExceptionstartup crash). - ❌ “Switching to CGLIB fixes self-invocation.” (Both JDK and CGLIB proxies fail identically on self-invocation).
- ❌ “TransactionTemplate requires an AOP proxy to create transactions.” (
TransactionTemplateusesPlatformTransactionManagerdirectly without proxies).