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()callsmethodB()internally, it is using thethisreference. 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
Controllersuddenly 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_NEWwas 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
methodBinto a separate@Servicebean. 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
@Transactionalannotations for fine-grained control, useTransactionTemplatefor 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
@Transactionalas 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.