User Safety: safe

Summary

A developer encountered a critical issue where Propagation.REQUIRES_NEW failed to initiate a separate transaction when called from within the same service class. Despite the explicit configuration, the second method executed within the scope of the first method’s transaction. This is a classic case of Self-Invocation bypassing the Spring AOP Proxy.

Root Cause

The failure occurs because of how Spring AOP (Aspect-Oriented Programming) implements declarative transaction management.

  • Proxy-Based Architecture: Spring does not inject your raw class into other beans; it injects a Proxy object that wraps your class.
  • Interception Mechanism: When an external caller invokes a method, the call hits the Proxy first. The Proxy then starts the transaction and calls the actual method in your target class.
  • The “Self-Invocation” Trap: When methodA() calls methodB() internally, it is using the this reference. This bypasses the Proxy entirely.
  • Direct Method Call: Because the call is a direct memory jump within the same object instance, the Transaction Interceptor (which resides in the Proxy) is never triggered for methodB().

Why This Happens in Real Systems

In complex, high-scale distributed systems, this behavior is a common source of data integrity bugs.

  • Implicit Dependencies: Developers often assume that annotations are “magic” and apply to every method in a class, forgetting that the Proxy is the gatekeeper.
  • Refactoring Side Effects: A method that worked fine when called from a Controller suddenly breaks its transactional logic when moved or refactored into an internal helper method within the same @Service.
  • Layered Architectures: As business logic grows, developers frequently create “orchestrator” methods that call “worker” methods. If both are in the same class, the worker’s transactional boundaries are ignored.

Real-World Impact

  • Atomicity Violations: A failure in a “required new” sub-task (like logging an audit trail or updating a counter) might trigger a rollback of the entire primary business transaction, which was the exact opposite of the intended design.
  • Data Corruption: If REQUIRES_NEW was intended to commit data independently (e.g., saving a state before a risky operation), and the primary transaction fails, that “independent” data is rolled back along with the rest, leading to loss of critical audit logs.
  • Silent Failures: These bugs rarely throw exceptions; they simply behave incorrectly, making them incredibly difficult to detect in production without heavy monitoring.

Example or Code

@Service
public class MyService {

    @Autowired
    private MyService self; // Strategy 1: Injecting the proxy itself

    @Transactional
    public void methodA() {
        // This call bypasses the proxy and uses the current transaction
        methodB(); 

        // This call uses the proxy and honors REQUIRES_NEW
        self.methodB(); 
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // Logic here
    }
}

How Senior Engineers Fix It

Senior engineers avoid “hacks” and instead apply structural patterns to ensure predictable behavior.

  • Class Separation (Preferred): The most robust fix is to move methodB into a separate @Service bean. This forces the call to go through the Spring Proxy.
  • Self-Injection: As shown in the code above, injecting the bean into itself (@Autowired private MyService self) allows you to call the method through the proxy reference. However, this can sometimes lead to circular dependency issues in older Spring versions.
  • TransactionTemplate: Instead of relying on declarative @Transactional annotations for fine-grained control, use TransactionTemplate for programmatic transaction management. This is much more explicit and less prone to proxy-related confusion.
  • Aspect-Oriented Testing: Write integration tests specifically designed to verify transactional boundaries (e.g., forcing an exception in the second method and checking if the first method’s data persisted).

Why Juniors Miss It

  • Mental Model Mismatch: Juniors often view @Transactional as a property of the method itself, whereas it is actually a property of the call through the proxy.
  • Lack of Proxy Knowledge: Most developers learn how to use Spring, but not how Spring works under the hood (CGLIB/JDK Dynamic Proxies).
  • Over-reliance on Declarative Magic: There is a tendency to treat annotations as “rules” that the JVM enforces, rather than “instructions” for a framework-level interceptor.

Leave a Comment