Why Spring @Transactional on a Called Method Doesn’t Rollback and How to Fix It
The article analyzes a Spring Boot transaction issue where a @Transactional method called from another non‑transactional method fails to roll back, explains the underlying propagation and proxy mechanisms, and presents two practical solutions—including using the bean’s proxy via AopContext with exposeProxy enabled—to ensure proper rollback.
The author encountered a Spring transaction problem: a scheduled job updates multiple records, using two service methods—method A (query and assemble) without @Transactional and method B (update) annotated with @Transactional. Although each record should roll back on failure, the rollback never occurred.
Understanding Spring Transaction Propagation
PROPAGATION_REQUIRED: Join the current transaction or create a new one if none exists (default). PROPAGATION_SUPPORTS: Join the current transaction if present; otherwise execute non‑transactionally. PROPAGATION_MANDATORY: Must run within an existing transaction; otherwise throw an exception. PROPAGATION_REQUIRES_NEW: Always start a new transaction, suspending any existing one. PROPAGATION_NOT_SUPPORTED: Execute non‑transactionally, suspending any existing transaction. PROPAGATION_NEVER: Execute only if no transaction exists; otherwise throw an exception. PROPAGATION_NESTED: Execute within a nested transaction if a transaction exists; otherwise behave like REQUIRED.
Spring’s default is PROPAGATION_REQUIRED. When method A is annotated with @Transactional, the transaction propagates to method B, the underlying JDBC connection has autoCommit=false, and rollback works as expected.
When method A lacks @Transactional, its connection uses autoCommit=true. The call to method B is a direct internal method invocation, which bypasses Spring’s dynamic proxy, so the @Transactional annotation on B is ignored and the update commits immediately.
Key insight: Internal calls within the same bean do not trigger Spring’s proxy‑based transaction management.
Solution Options
1. Extract method B into a separate service and inject that service, allowing Spring to create a proxy for the transactional method. This works but adds extra classes and boilerplate.
2. Invoke method B through the bean’s proxy from within the same class . This can be done in two ways:
Obtain the bean from the ApplicationContext and call the method on that instance.
Use AopContext.currentProxy() after enabling exposeProxy=true in the AOP configuration.
The author chose the second approach. The required steps are: @EnableAspectJAutoProxy(exposeProxy = true) Then, inside the service:
import org.springframework.aop.framework.AopContext;
@Service
public class MyService {
public void methodA() {
// ... query and assemble data ...
((MyService) AopContext.currentProxy()).methodB();
}
@Transactional
public void methodB() {
// update logic that may throw RuntimeException to trigger rollback
}
}After applying the proxy‑based call, the transaction rolls back correctly when an exception is thrown in method B.
With this fix, the scheduled update behaves as intended: each failed record is rolled back without affecting other records.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
