Learnitweb

Why @Transactional Doesn’t Work for Internal Calls in Spring Beans

This is a classic and very important “gotcha” in Spring’s transaction management.

If a method in a Spring bean calls another @Transactional method from within the same bean instance, the transaction annotation on the called method is ignored.

Here’s why this happens and what you need to know:

The Root Cause: Spring’s Proxy-Based AOP

Spring’s @Transactional annotation works using Aspect-Oriented Programming (AOP). By default, Spring creates a proxy object that wraps your actual bean. This proxy is what intercepts the method calls.

  • When a method on your bean is called from another bean, the call goes through this proxy. The proxy sees the @Transactional annotation and starts a new transaction (or joins an existing one) before delegating the call to your actual method. It then handles the commit or rollback after the method returns.
  • When a method is called from within the same bean instance (e.g., this.myTransactionalMethod()), the call is a direct method invocation on the actual bean instance. It completely bypasses the Spring proxy. Therefore, the AOP logic, including the transaction management, is never applied.

Practical Implications

Let’s illustrate with an example:

@Service
public class UserService {

    @Transactional
    public void createUserAndAudit(User user) {
        // This is a transactional method
        saveUser(user);
        auditAction("User created: " + user.getUsername());
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void auditAction(String action) {
        // This is a transactional method with REQUIRES_NEW
        // We expect it to run in a separate transaction.
        // ... some database call to save the audit log
    }

    private void saveUser(User user) {
        // ... some database call to save the user
    }
}

If you call userService.createUserAndAudit() from another bean, both saveUser and auditAction will be executed within the same transaction started by createUserAndAudit. The @Transactional(propagation = Propagation.REQUIRES_NEW) on auditAction is completely ignored because the call this.auditAction() is an internal call that doesn’t go through the proxy.

This means if the saveUser call fails and rolls back, the auditAction will also be rolled back, which may not be the desired behavior.

Solutions to this Problem

There are a few ways to solve this, each with its own pros and cons:

  1. Move the @Transactional method to another bean.
    This is the most common and often cleanest solution. By moving the auditAction method to a separate AuditService bean, you ensure that the call from UserService to AuditService goes through the proxy and the transaction logic is applied correctly.
  2. Use AspectJ Mode for Transactions.
    This is a more advanced solution. Spring can be configured to use AspectJ weaving, which modifies the bytecode of the class at compile time or load time. This means the transaction logic is “woven” directly into the class, so even internal method calls will be intercepted.
    • This requires additional configuration and a build-time dependency.
    • It is a powerful solution but adds more complexity to your build process.
  3. Self-inject the bean.
    You can inject the bean into itself. This creates a reference to the proxy of the bean, allowing you to call its methods via the proxy.
@Service
public class UserService {
    @Autowired
    private UserService self; // Injecting the proxy of the bean

    @Transactional
    public void createUserAndAudit(User user) {
        self.saveUser(user); // Call through the proxy
        self.auditAction("User created: " + user.getUsername()); // Call through the proxy
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void auditAction(String action) {
        // ...
    }
}