Understanding Spring Transaction Propagation, Rollback, and @Transactional Usage
This article explains Spring's transaction propagation options, rollback mechanisms, the limitations of self‑invocation within @Transactional methods, and how to correctly use AOP proxies such as AopContext.currentProxy() and @EnableAspectJAutoProxy to ensure transactional behavior in Java backend applications.
The author, a senior architect, shares practical insights on using Spring's @Transactional annotation after several days of testing and debugging.
Transaction propagation behaviors are defined in TransactionDefinition and include:
TransactionDefinition.PROPAGATION_REQUIRED : Join existing transaction or create a new one (default).
TransactionDefinition.PROPAGATION_REQUIRES_NEW : Always start a new transaction, suspending any existing one.
TransactionDefinition.PROPAGATION_SUPPORTS : Join if a transaction exists; otherwise execute non‑transactionally.
TransactionDefinition.PROPAGATION_NOT_SUPPORTED : Execute non‑transactionally and suspend any existing transaction.
TransactionDefinition.PROPAGATION_NEVER : Throw an exception if a transaction exists.
TransactionDefinition.PROPAGATION_MANDATORY : Join existing transaction; throw if none.
TransactionDefinition.PROPAGATION_NESTED : Execute within a nested transaction or behave like PROPAGATION_REQUIRED when no outer transaction exists.
Rollback mechanism : Spring's declarative transaction management rolls back on unchecked (runtime) exceptions. The transaction boundary starts before the business method and commits or rolls back after the method finishes, depending on whether a RuntimeException was thrown.
If a method catches exceptions with try { } catch (Exception e) { } , the caught block runs outside the transaction; to trigger rollback you must re‑throw a RuntimeException inside the catch block.
Self‑invocation limitation : Calling another @Transactional method from within the same class using this.method() bypasses the proxy, so the annotation has no effect. The solution is to obtain the proxy via AopContext.currentProxy() and invoke the method on that proxy. This requires enabling proxy exposure with @EnableAspectJAutoProxy(exposeProxy = true) and ensuring the methods are public .
Example code snippets:
@RestController
public class TransactionalController {
@Autowired
private TransactionalService transactionalService;
@PostMapping("transactionalTest")
public void transacionalTest() {
transactionalService.transactionalMethod();
}
} public interface TransactionalService {
void transactionalMethod();
}Both the controller and service examples illustrate how the transaction is started, how multiple update operations can be rolled back together, and how to correctly structure code to keep transaction consistency.
In summary, @Transactional ensures each annotated method runs within a transaction, the method must be public , self‑invocation does not trigger AOP advice, and using the proxy obtained from AopContext.currentProxy() or proper interface‑based proxies resolves the issue.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.