Why Many Large Companies Discourage Using @Transactional in Spring
The article explains common pitfalls that cause Spring @Transactional to fail or not roll back, such as incorrect method visibility, final or static modifiers, internal method calls, beans not managed by Spring, multithreading, unsupported database engines, misconfigured propagation, swallowed exceptions, and improper rollback settings, and offers practical solutions for each case.
1. Transaction Not Effective
Spring creates a proxy for each bean that participates in AOP. Only public methods can be intercepted. If a method is private, protected, package‑private, or final, the proxy cannot apply the @Transactional interceptor, so no transaction is started.
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) { // not intercepted
saveData(userModel);
updateData(userModel);
}
}Static methods suffer the same limitation because they cannot be proxied.
Static methods cannot be wrapped by Spring AOP and therefore cannot participate in declarative transactions.
Internal method calls bypass the proxy. When a method in the same class invokes another @Transactional method via this, the call is not routed through the proxy and the inner method runs without a transaction.
@Service
public class UserService {
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel); // direct call, no transaction
}
@Transactional
public void updateStatus(UserModel userModel) {
// transactional logic
}
}Work‑arounds:
Extract the transactional logic into a separate @Service bean and invoke it.
Inject the bean into itself and call the injected proxy.
Use AopContext.currentProxy() to obtain the proxy and invoke the method.
If a class is not annotated with @Component, @Service, @Controller, etc., Spring does not create a bean, so no proxy and no transaction are applied.
//@Service // missing annotation
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}Spring binds a database connection to the current thread via a ThreadLocal. When a transactional method spawns a new thread, the child thread receives a different connection, resulting in two independent transactions. An exception in the child thread does not cause a rollback of the parent transaction.
@Service
public class UserService {
@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");
}
}MySQL tables that use the MyISAM engine do not support transactions; operations on such tables are never rolled back even if the surrounding method is annotated.
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) DEFAULT NULL,
`two_category` varchar(20) DEFAULT NULL,
`three_category` varchar(20) DEFAULT NULL,
`four_category` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;In a plain Spring (non‑Boot) project you must configure a transaction manager and an AOP pointcut. Forgetting this configuration disables transaction handling.
2. Transaction Not Rolling Back
2.1 Wrong propagation
Only propagation values that create a transaction ( REQUIRED, REQUIRES_NEW, NESTED) start a new transaction. Using Propagation.NEVER or other non‑creating types prevents a transaction from being started.
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}2.2 Swallowing exceptions
If a transactional method catches an exception and does not re‑throw it, Spring treats the method as successful 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 non‑runtime exceptions
By default Spring rolls back only on RuntimeException and Error. Throwing a checked Exception does 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 Misconfigured rollbackFor
If rollbackFor lists an exception type that is never thrown, the transaction will not roll back for the actual exception (e.g., SqlException).
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}To guarantee rollback for any error, set rollbackFor = Exception.class or Throwable.class.
2.5 Nested transaction rollback scope
When an inner method annotated with propagation = Propagation.NESTED throws an exception, the outer transaction also rolls back because the exception propagates to the outer proxy.
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
roleService.doOtherThing(); // NESTED
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("save role data");
}
}To roll back only the inner transaction, catch the exception inside the outer method and prevent it from bubbling up.
3. Other Considerations
3.1 Large‑transaction problem
Annotating an entire service method with @Transactional may unintentionally include many read‑only queries, leading to long‑running transactions. Extract the truly transactional statements into a separate method or service to keep the transaction scope small.
3.2 Programmatic (declarative vs. programmatic) transactions
Spring provides TransactionTemplate for programmatic transaction management, which avoids proxy‑related pitfalls and gives finer‑grained control.
@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, TransactionTemplate is preferable for complex scenarios where proxy limitations might cause transaction loss.
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.
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.
