Why Your Spring @Transactional Might Fail: 7 Common Pitfalls and How to Fix Them
This article explains why Spring transactions can become ineffective or fail to roll back, covering issues such as wrong method visibility, final methods, internal calls, missing Spring bean registration, multithreading, unsupported table engines, misconfigured propagation, exception handling, and offers practical solutions for each case.
Introduction
For Java developers, Spring transactions are familiar. When a request needs to write multiple tables atomically, we use @Transactional. However, misuse can cause the transaction to fail.
1. Transaction Not Effective
1.1 Access Modifier Issue
Spring requires the proxied method to be public; if it is private, protected or package‑private, the transaction is ignored.
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}The AbstractFallbackTransactionAttributeSource class checks the method modifiers and returns null for non‑public methods, so no transaction attribute is applied.
1.2 Method Declared final
Final methods cannot be overridden by the proxy generated by Spring AOP, therefore the transaction logic is never added.
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Note: static methods also cannot be proxied.
1.3 Internal Method Call
Calling a transactional method from another method in the same class uses the this reference, bypassing the proxy, so the transaction does not take effect.
Solutions:
3.1 Add a New Service Method
@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();
}
}3.2 Inject Self
@Service
public class ServiceA {
@Autowired
private ServiceA self;
public void save(User user) {
queryData1();
queryData2();
self.doSave(user);
}
@Transactional(rollbackFor = Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}3.3 Use AopContext.currentProxy()
@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
Only beans created by Spring (annotated with @Service, @Component, @Repository, etc.) are proxied. If a class lacks such an annotation, @Transactional has no effect.
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}1.5 Multithreaded Calls
Transactions are bound to the current thread via a ThreadLocal. Invoking transactional code in another thread creates a separate connection, so rollback does not propagate.
@Slf4j
@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");
}
}The transaction resources are stored in a ThreadLocal<Map<Object, Object>>, so different threads use different connections.
1.6 Table Does Not Support Transaction
MyISAM engine (used before MySQL 5) does not support transactions. Using such tables prevents transaction commit/rollback.
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;When a table’s engine does not support transactions, the transaction will never be effective.
1.7 Transaction Not Enabled
In Spring Boot, DataSourceTransactionManagerAutoConfiguration enables transaction management automatically. In a traditional Spring project you must configure a DataSourceTransactionManager bean and tx:advice in XML.
<!-- Configure transaction manager -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- Apply advice to beans -->
<aop:config>
<aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>If the pointcut expression is wrong, some beans will not be proxied and their transactions will not work.
2. Transaction Not Rolling Back
2.1 Wrong Propagation Setting
Using a propagation type that does not create a transaction (e.g., Propagation.NEVER) will prevent rollback.
@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 them makes Spring think the method completed successfully, so no rollback occurs.
@Slf4j
@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 the Wrong Exception Type
Spring rolls back only on RuntimeException or Error by default. Throwing a checked Exception does not trigger rollback.
@Slf4j
@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 is never thrown, the transaction will not roll back when other exceptions occur.
@Slf4j
@Service
public class UserService {
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}
}SQL exceptions such as DuplicateKeyException will not trigger rollback in this configuration.
2.5 Nested Transaction Rollback Propagation
When a nested transaction throws an exception that propagates to the outer transaction, the whole transaction is rolled back unless the inner exception is caught and not re‑thrown.
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("save role data");
}
}Solution: wrap the inner call in a try/catch block and do not re‑throw the exception.
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}3. Other Issues
3.1 Large Transaction Problem
Annotating an entire method can unintentionally include many read‑only queries, leading to long‑running transactions and performance problems.
3.2 Programmatic Transaction
Using TransactionTemplate gives explicit control over transaction boundaries 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 programmatic transactions for complex scenarios, but @Transactional remains convenient for simple use cases.
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.
