🍃 Section 5 · Question #19

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 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:

  1. Without @Lazy, attempting to inject UserService into UserService causes Spring to detect an unresolvable cycle and crash with BeanCurrentlyInCreationException.
  2. When @Lazy is present on the constructor parameter, Spring generates a synthetic CGLIB proxy placeholder for self and completes constructor instantiation immediately.
  3. When self.saveUserInNewTransaction() is first called, the synthetic proxy delegates to the BeanFactory to fetch the fully initialized singleton proxy for UserService.

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:

  1. 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 throws IllegalStateException: Cannot find current proxy at runtime.
  2. Global Performance Penalty: Setting exposeProxy = true forces every single dynamic proxy across the entire Spring context to publish its reference to a ThreadLocal on every method invocation, wasting CPU cycles and memory.
  3. Breaks Unit Testing: Plain JUnit tests (new OrderService().outerMethod()) immediately crash because AopContext is 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 (@Lazy self-injection or AopContext).
  • 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:

  1. Configuration Fragility: It depends on the global property exposeProxy = true on @EnableAspectJAutoProxy. If omitted, the method fails at runtime with IllegalStateException.
  2. Performance Overhead: Enabling exposeProxy causes Spring’s AOP interceptors to write to a ThreadLocal on every proxy call across the entire application, adding CPU and garbage collection overhead.
  3. 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 this execute directly on the target instance’s vtable without passing through the MethodInterceptor chain. The 4 ways to solve it:

  1. Extract Method to a Separate Bean (Best Practice): Refactor the transactional logic into a new collaborator service (clean SRP, easily testable).
  2. TransactionTemplate: Manage transactions programmatically with lambda callbacks (bypasses proxies entirely; critical for isolating transactions from slow HTTP calls).
  3. Self-Injection (@Lazy Constructor / ObjectProvider): Inject the bean’s own proxy reference to call self.method() (useful for legacy refactoring).
  4. 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 fatal BeanCurrentlyInCreationException startup 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.” (TransactionTemplate uses PlatformTransactionManager directly without proxies).