Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 48

AOP Proxies, Declarative Transactions, and Self-Invocation

Recommended resources

Hello! Welcome to the final lesson in our module on Spring Data and Transaction Management.

In our last session, we dug into how Spring decides to roll back a transaction, focusing on the crucial difference between checked and unchecked exceptions. We established that this entire mechanism relies on Spring creating a proxy around your service bean to intercept method calls and apply transactional logic.

Today, we will pull back the curtain on this proxy mechanism. Your goal is to explain how AOP proxies enable Spring's declarative transaction management and diagnose the common 'self-invocation problem.'

Understanding this is a significant step up from simply using @Transactional. It demonstrates a deep, architectural understanding of the Spring Framework, which is precisely what interviewers at top-tier companies look for in mid-to-senior candidates.

1. The Magic Behind @Transactional: AOP Proxies

Declarative transaction management is a prime example of Aspect-Oriented Programming (AOP). AOP allows you to modularize "cross-cutting concerns"—functionality that spans multiple points in an application, such as logging, security, and transactions. Instead of scattering transaction management code throughout your business logic, you declare it with an annotation, and Spring applies it "from the outside."

It does this using a proxy object. When another bean calls your @Transactional method, it's not calling your object directly. It's calling a proxy that Spring generated. This proxy wraps your actual object, starts a transaction, calls your method, and then commits or rolls back the transaction.

AOP Proxy Mechanism with Logging Aspect
This diagram illustrates the core idea. A call from a `CustomerService` is intercepted by a `CustomerDaoProxy`. The proxy first invokes an aspect (like starting a transaction or logging), and only then delegates the call to the actual `CustomerDao` object containing the business logic.

But how does Spring create this proxy object at runtime? It uses one of two strategies.

Spring Boot In Depth : 6 - How Spring handles Transactions & Security using AOP/Proxies

To understand these proxy creation strategies, let's watch a video that explains the concepts from first principles. It starts by defining cross-cutting concerns and then dives into the two dynamic proxy mechanisms Spring uses.

This video is broken into a few key segments. First, watch from 00:00 to 02:14 to get a clear definition of cross-cutting concerns. Then, skip ahead and watch from 08:17 to 17:28. This is the core of the section. Focus on: JDK Dynamic Proxies (8:17 - 12:53): Understand how they work and their key limitation—they require the target object to implement an interface. CGLIB Proxies (12:53 - 17:28): See how CGLIB overcomes the interface limitation by creating a subclass. Note that the video correctly states that Spring uses CGLIB internally.

To summarize the key points from the video and general Spring knowledge:

  • JDK Dynamic Proxy: This is Java's built-in way of creating proxies. It's powerful but has a major restriction: it can only proxy objects that implement an interface. The proxy itself is a new class generated at runtime that implements the same interface as your target object.
  • CGLIB (Code Generation Library): This is a third-party library that Spring bundles. CGLIB creates a proxy by generating a subclass of your target class at runtime. By overriding methods in the subclass, it can add the AOP advice (like transaction management). Because it uses inheritance, it doesn't require your class to implement an interface.

In modern Spring Boot (version 2.x and later), CGLIB is the default proxying mechanism, regardless of whether your class implements an interface or not. This provides more consistent behavior across all your beans.

Spring AOP Proxy Implementations
This diagram visually summarizes the two proxy strategies. The `AopProxy` interface has two main implementations: `JdkDynamicAopProxy`, which uses Java's built-in reflection and requires interfaces, and `CglibAopProxy`, which uses the CGLIB library to create a subclass. CGLIB is the more flexible and default option in modern Spring.

2. The Self-Invocation Problem: When Proxies Are Bypassed

Now that we know an external proxy object is intercepting calls, we can address a classic interview question: "What happens if a @Transactional method calls another @Transactional method within the same class?"

The answer leads us to the self-invocation problem.

Let's imagine you have a PaymentService:

@Service
public class PaymentService {

    @Transactional
    public void processPaymentAndNotify() {
        // ... some logic to save payment details ...
        
        // Internal call to another method in the same class
        this.sendNotification(); 
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification() {
        // ... logic to send a notification in a new, separate transaction ...
    }
}

You would expect sendNotification() to run in a new transaction because of Propagation.REQUIRES_NEW. However, it will not. It will run in the same transaction as processPaymentAndNotify().

Why? Because the call this.sendNotification() is an internal method call on the raw PaymentService object. It does not go through the proxy. The proxy only intercepts calls that come from outside the object (e.g., from a controller or another service).

Spring Boot In Depth : 6 - How Spring handles Transactions & Security using AOP/Proxies

The same video we watched earlier has an excellent segment explaining this exact problem with a clear diagram.

Please watch from 20:55 to 25:50. The video clearly illustrates how a call from an external client (like a controller) hits the proxy, but an internal call (this.anotherMethod()) bypasses the proxy entirely, causing its transactional annotations to be ignored.

Test your understanding!

Consider the self-invocation problem we just discussed. This issue is not unique to @Transactional. Based on your understanding of AOP proxies, what other common Spring annotations might be affected by this same problem?

Show answer

Any annotation that relies on Spring's AOP proxy mechanism will be affected. Common examples include:

  • @Cacheable, @CacheEvict, @CachePut for declarative caching.
  • @Async for asynchronous method execution.
  • @Retryable from Spring Retry.
  • Method-level security annotations like @PreAuthorize or @Secured.

In all these cases, a self-invocation will bypass the proxy, and the intended behavior (caching, async execution, security check, etc.) will not be applied to the internal method call.


3. Diagnosing and Solving the Self-Invocation Problem

In an interview, after you explain the problem, the follow-up question will inevitably be, "So how do you fix it?" Here are the standard solutions.

Spring Transaction Internals — Why @Transactional sometimes “doesn’t work”?

This article offers a concise summary of the problem and presents the most common and accepted solutions.

Read the short section under the heading 'Gotcha: Why @Transactional sometimes “doesn’t work”?'. It explains the problem and introduces two key solutions: extracting the method to another bean or using self-injection.

As the article describes, the goal is to ensure the call goes through the proxy.

Solution 1: Refactor to a Separate Bean (Recommended)

This is the cleanest and most common solution. You move the internal method to its own dedicated Spring bean and inject that new bean into your original service.

Before:

@Service
public class PaymentService {
    @Transactional
    public void processPaymentAndNotify() {
        // ...
        this.sendNotification(); // Bypasses proxy
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification() { /* ... */ }
}

After (Correct):

@Service
public class NotificationService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification() { /* ... */ }
}

@Service
public class PaymentService {
    private final NotificationService notificationService;

    // ... constructor injection ...

    @Transactional
    public void processPaymentAndNotify() {
        // ...
        notificationService.sendNotification(); // Goes through the proxy!
    }
}

Now, the call to notificationService.sendNotification() is an external call to another bean, so it is correctly intercepted by the NotificationService's proxy, and a new transaction is created as intended.

Solution 2: Self-Injection

A second, less common approach is to inject the bean into itself. This forces the call to go through the application context, which returns the proxy.

@Service
public class PaymentService {

    @Autowired
    private PaymentService self; // Inject the proxy of this service into itself

    @Transactional
    public void processPaymentAndNotify() {
        // ...
        self.sendNotification(); // Call on the proxy, not 'this'
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendNotification() { /* ... */ }
}

While this works, some developers find it confusing. It can also create issues with circular dependencies if not managed carefully (though Spring can typically handle this specific case of self-injection). The separate bean approach is generally preferred for clarity.


Conclusion

You've now completed a deep dive into the AOP proxy mechanism that powers much of Spring's "magic," including declarative transactions. This is a fundamental concept that separates a junior user of the framework from a senior developer who understands its architecture.

Key Takeaways:

  • Spring enables @Transactional using AOP proxies, which intercept method calls to add transactional behavior.
  • Spring's default proxy strategy is CGLIB, which creates a subclass of your bean at runtime. This avoids the need for interfaces.
  • The self-invocation problem occurs when an internal method call (this.anotherMethod()) bypasses the proxy, causing AOP-driven annotations like @Transactional on the called method to be ignored.
  • The recommended solution is to refactor the internal method into a separate bean to ensure the call is external and thus intercepted by the proxy.

Preview of the Next Lesson:
This lesson concludes our module on Spring Data and Transaction Management. We've seen how AOP handles the cross-cutting concern of transactions. In our next module, "Spring Security & REST API Design," we'll explore another critical cross-cutting concern: security. Our first lesson will be to explain Spring Security's filter chain architecture and the role of the SecurityContextHolder, another powerful interception-based mechanism.