Spring Transaction Pitfalls: Common Scenarios Where Transactions Fail and How to Fix Them
This article explains the most common reasons why Spring @Transactional annotations may become ineffective or fail to roll back, covering access‑modifier issues, final methods, internal calls, self‑injection, multithreading, unsupported table engines, mis‑configured propagation, exception handling, nested transactions, large‑transaction problems, and the advantages of programmatic transaction management.
For Java developers, Spring transaction management is a familiar tool, but it can silently fail if used incorrectly. This article lists the typical situations where @Transactional does not work as expected and provides practical solutions.
1. Transaction Not Effective
1.1 Access‑modifier problem
Spring only creates proxies for public methods. If a transactional method is declared private , protected or package‑private, Spring returns null for the transaction attribute, so no transaction is applied.
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}1.2 Method declared final
When a method is final , the generated proxy cannot override it, therefore the transactional logic is never inserted.
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}1.3 Internal method call
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 {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel); // no transaction here
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}1.4 Adding a separate service
Move the transactional logic to another @Service bean and inject it, so the call goes through the proxy.
@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.5 Self‑injection
Inject the bean into itself (Spring’s three‑level cache prevents circular‑dependency problems) and call the transactional method via the injected proxy.
@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();
}
}1.6 Using AopContext.currentProxy()
Obtain the current proxy programmatically and invoke the transactional method through it.
@Service
public class ServiceA {
public void save(User user) {
((ServiceA) AopContext.currentProxy()).doSave(user);
}
@Transactional(rollbackFor = Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}1.7 Bean not managed by Spring
If a class lacks @Component , @Service , @Repository or similar annotations, Spring will not create a proxy, and @Transactional has no effect.
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}1.8 Multithreading
Transactional context is bound to the current thread. When a new thread is started, it receives a different database connection, so the operations run in separate transactions and cannot roll back together.
@Service
public class UserService {
@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");
}
}1.9 Table engine does not support transactions
MyISAM tables (MySQL 5 and earlier) cannot participate in transactions. Using such tables will make any @Transactional annotation ineffective.
CREATE TABLE `category` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`one_category` VARCHAR(20) DEFAULT NULL,
...
) ENGINE=MyISAM;1.10 Transaction not enabled
In a plain Spring project you must configure a transaction manager and enable @Transactional via XML or Java config. Spring Boot does this automatically through DataSourceTransactionManagerAutoConfiguration .
2. Transaction Not Rolling Back
2.1 Wrong propagation attribute
Using Propagation.NEVER or other non‑creating propagation types prevents a transaction from being started, so rollback never occurs.
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}2.2 Swallowing exceptions
If the method catches the exception and does not re‑throw it, Spring assumes the operation succeeded and will not roll back.
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}2.3 Throwing a non‑runtime exception
By default Spring rolls back only on RuntimeException and Error . Throwing a checked Exception will not trigger a rollback unless rollbackFor is configured.
@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 Mis‑configured rollbackFor
If rollbackFor is set to a custom exception that never occurs, the transaction will not roll back for the actual database exceptions.
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}2.5 Nested transaction roll‑back behavior
When an inner transaction (propagation = NESTED) throws an exception that propagates outward, the outer transaction also rolls back, unless the exception is caught and suppressed.
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing(); // NESTED transaction
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("save role data");
}
}3. Other Important Topics
3.1 Large‑transaction problem
Annotating a whole service method with @Transactional may unintentionally include many read‑only queries, leading to long‑running transactions and performance issues. It is better to isolate the transactional part into a dedicated method or service.
3.2 Programmatic (declarative vs. programmatic) transactions
Spring also provides TransactionTemplate for programmatic transaction control, which avoids AOP proxy pitfalls and gives finer‑grained scope.
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute(status -> {
addData1();
updateData2();
return Boolean.TRUE;
});
}While @Transactional is convenient for simple cases, using TransactionTemplate is recommended for complex or performance‑critical scenarios.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
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.