Why Spring Transactions Fail: 7 Common Pitfalls and How to Fix Them
This article explores why Spring transactions may fail, covering seven common pitfalls such as incorrect method visibility, final methods, internal calls, missing Spring management, multithreading, unsupported table engines, and misconfigured propagation, and provides practical solutions to ensure reliable transaction handling.
Preface
For Java developers, Spring transactions are familiar. In scenarios where a request needs to write to multiple tables, Spring transactions ensure atomicity. While @Transactional makes it easy, misuse can cause subtle problems.
1. Transaction Not Effective
1.1 Access Modifier Issue
Spring requires the proxied method to be public. If a @Transactional method is declared private, default, or protected, the transaction will be ignored because AbstractFallbackTransactionAttributeSource returns null for non‑public methods.
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Therefore, the method must be public.
1.2 Final Method
Marking a transactional method as final prevents Spring AOP from creating a proxy that can override the method, so the transaction is not applied.
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Note: static methods also cannot be proxied for transactions.
1.3 Internal Method Call
Calling another @Transactional method from within the same class uses the current (this) instance, bypassing the proxy, so the second method runs without a transaction.
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}Solutions include extracting the called method to another @Service, injecting the current bean, or using AopContext.currentProxy().
1.3.1 Extract to Another Service
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Service
public class ServiceB {
@Transactional(rollbackFor = Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}1.3.2 Inject Self
@Service
public class ServiceA {
@Autowired
private ServiceA serviceA;
public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor = Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}1.3.3 Use AopContext
@Service
public class ServiceA {
public void save(User user) {
queryData1();
queryData2();
((ServiceA) AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor = Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}1.4 Not Managed by Spring
If a class is not annotated with @Service, @Component, etc., Spring will not create a bean, and @Transactional will have no effect.
// @Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}1.5 Multithreaded Calls
Transactional methods executed in a new thread do not share the same database connection, so they run in separate transactions and cannot roll back together.
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("Save role data");
}
}2. Transaction Not Rolling Back
2.1 Wrong Propagation Setting
Using Propagation.NEVER disables transaction creation, causing the method to run without a transaction.
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}2.2 Swallowing Exceptions
Catching exceptions without re‑throwing prevents Spring from marking the transaction for rollback.
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}2.3 Throwing Non‑Runtime Exceptions
By default Spring rolls back only on RuntimeException and Error. Throwing a checked Exception does not trigger rollback.
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}2.4 Misconfigured rollbackFor
If rollbackFor is set to a custom exception that never occurs, the transaction will not roll back for database exceptions.
@Service
public class UserService {
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}
}2.5 Nested Transaction Rollback Scope
When a nested transaction throws an exception that propagates outward, the outer transaction also rolls back. To isolate the rollback, catch the exception inside the outer method.
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}3. Other Topics
3.1 Large Transaction Problems
Annotating whole service methods with @Transactional can unintentionally include many read‑only queries, leading to long‑running transactions and performance issues. It is better to limit the transactional scope to the exact statements that modify data.
3.2 Programmatic Transactions
Spring also supports programmatic transaction management via TransactionTemplate, which offers finer control and avoids AOP proxy pitfalls.
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute(status -> {
addData1();
updateData2();
return Boolean.TRUE;
});
}Prefer TransactionTemplate for complex scenarios, but @Transactional remains convenient for simple, stable business logic.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.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.
