When Spring @Transactional Fails: Common Pitfalls and Fixes
This article explains why Spring @Transactional may not work as expected, covering issues such as wrong method visibility, final methods, internal calls, missing bean registration, multithreading, unsupported table engines, incorrect propagation settings, swallowed exceptions, and how to resolve each problem.
Transaction Not Effective
Method Visibility
Spring creates a proxy for a bean and only public methods can be intercepted. If a @Transactional method is declared private, protected or package‑private, Spring returns null for the transaction attribute and the method runs without a transaction.
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Final Methods
Methods marked final cannot be overridden by the proxy class, so the transactional advice is never applied.
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Static methods suffer the same problem because they cannot be proxied.
Internal Method Calls
Calling a @Transactional method from another method of the same class uses the this reference, bypassing the proxy. Consequently, the inner method runs without a transaction.
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateStatus(userModel); // internal call – no transaction
}
@Transactional
public void updateStatus(UserModel userModel) {
// transactional logic
}
}Solutions:
Add a separate service method and inject it, then call the new bean.
Inject the service into itself (self‑injection) and call the method on the injected proxy.
Use AopContext.currentProxy() to obtain the proxy and invoke the method.
// Self‑injection example
@Service
public class ServiceA {
@Autowired
private ServiceA self;
public void save(User user) {
// ...
self.doSave(user); // goes through proxy
}
@Transactional
public void doSave(User user) {
// transactional work
}
}Bean Not Managed by Spring
If a class lacks a stereotype annotation such as @Service, Spring will not create a bean for it, and any @Transactional method inside will never be proxied.
// Missing @Service – no transaction
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Multithreaded Calls
Transactional context is bound to the current thread. When a transaction‑annotated method spawns a new thread and invokes another @Transactional method inside that thread, the second method runs without the original transaction and cannot roll back the outer work.
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
new Thread(() -> roleService.doOtherThing()).start();
}
}
@Service
public class RoleService {
@Transactional
public void doOtherThing() {
System.out.println("save role data");
}
}Table Engine Does Not Support Transactions
MyISAM tables in MySQL do not support transactions. If a table used in a multi‑table operation is MyISAM, the operation cannot be rolled back, leading to data inconsistency.
CREATE TABLE `category` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`one_category` VARCHAR(20),
`two_category` VARCHAR(20),
`three_category` VARCHAR(20),
`four_category` VARCHAR(20),
PRIMARY KEY (`id`)
) ENGINE=MyISAM;Always verify that the underlying tables use a transactional engine such as InnoDB before relying on Spring transactions.
Transaction Not Enabled (XML Configuration)
In a plain Spring project you must configure a DataSourceTransactionManager and enable transaction advice. Missing or incorrect configuration (e.g., wrong pointcut expression) will prevent transactions from being applied.
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(* com.example..*.*(..))"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>Transaction Does Not Roll Back
Wrong Propagation Setting
Using a propagation type that does not create a transaction (e.g., Propagation.NEVER) will cause the method to run without a transaction, so no rollback can occur.
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Swallowing Exceptions
If a @Transactional method catches exceptions and does not re‑throw them, Spring assumes the method completed successfully and will not roll back.
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
// no re‑throw → no rollback
}
}Throwing Non‑Rollback Exceptions
By default Spring rolls back only on RuntimeException and Error. Throwing a checked Exception without configuring rollbackFor will not trigger a rollback.
@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); // still a checked exception → no rollback
}
}Misconfigured rollbackFor
If rollbackFor is set to a custom exception that never occurs, the transaction will not roll back for the actual database exceptions (e.g., SQLException).
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
// SQLException is thrown → not rolled back because it is not BusinessException
}Nested Transaction Rollback Scope
When using Propagation.NESTED, an exception in the inner transaction propagates to the outer transaction unless it is caught. To roll back only the inner transaction, catch the exception inside the outer method.
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing(); // NESTED transaction
} catch (Exception e) {
log.error(e.getMessage(), e);
// inner transaction rolled back, outer continues
}
}Other Transaction Topics
Large Transaction Problem
Annotating a whole method with @Transactional can unintentionally include many read‑only queries, leading to long‑running transactions and performance issues. Only the statements that modify data should be placed inside a transaction.
// Only the two lines below really need a transaction
roleService.save(userModel);
update(userModel);Programmatic (Declarative vs. Programmatic) Transactions
Spring also provides programmatic transaction management via TransactionTemplate. This approach gives finer control over transaction boundaries and avoids proxy‑related pitfalls.
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute(status -> {
addData1();
updateData2();
return Boolean.TRUE;
});
}Use programmatic transactions when you need precise control or when AOP proxy limitations cause @Transactional to fail.
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.
