Common @Transactional Pitfalls in Spring Boot Business Code and How to Avoid Them
This article analyzes frequent scenarios where Spring's @Transactional annotation fails in Spring Boot projects—such as self-invocation, swallowed exceptions, misconfigured rollback rules, non‑public methods, wrong propagation settings, and long‑running transactions—and provides concrete fixes to prevent data inconsistency and performance issues.
Overview
Spring Boot enables declarative transaction management through the @Transactional annotation. When the annotation is not applied correctly, the transaction may never start or may fail to roll back, causing data inconsistency and performance degradation.
@Transactional annotation definition
The annotation can be placed on a class or on individual methods. Its key attributes are:
propagation : determines how a transaction behaves when a method calls another transactional method. Default is Propagation.REQUIRED. Other options include REQUIRES_NEW, SUPPORTS, NOT_SUPPORTED, NEVER, MANDATORY, and NESTED.
isolation : sets the isolation level. Default is Isolation.DEFAULT (e.g., MySQL uses REPEATABLE_READ). Common levels are READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, and SERIALIZABLE.
timeout : transaction timeout in seconds; -1 means no timeout.
readOnly : marks a transaction as read‑only.
rollbackFor / noRollbackFor : specify exception types that trigger or suppress rollback.
Failure scenarios and remedies
3.1 Self‑invocation within the same class
public void addUser(UserParam param) {
User user = PtcBeanUtils.copy(param, User.class);
userDAO.insert(user);
// self‑call bypasses proxy
this.addUserRole(user.getId(), param.getRoleIds());
}
@Transactional(rollbackFor = Exception.class)
public void addUserRole(Long userId, List<Long> roleIds) {
if (CollectionUtils.isEmpty(roleIds)) {
return;
}
List<UserRole> userRoles = new ArrayList<>();
roleIds.forEach(roleId -> {
UserRole ur = new UserRole();
ur.setUserId(userId);
ur.setRoleId(roleId);
userRoles.add(ur);
});
userRoleDAO.insertBatch(userRoles);
throw new RuntimeException("exception occurred");
}Calling addUserRole via this invokes the original object, not the Spring proxy, so the transaction is never started and the exception does not trigger a rollback.
Fix : Obtain the bean from the Spring container (e.g., inject UserService and call userService.addUserRole(...)) so that the call goes through the proxy.
3.2 Swallowing exceptions
@Transactional(rollbackFor = Exception.class)
public void addUser(UserParam param) {
try {
User user = PtcBeanUtils.copy(param, User.class);
// ... business logic ...
this.addUserRole(user.getId(), param.getRoleIds());
} catch (Exception e) {
log.error(e.getMessage()); // exception consumed
}
}
@Transactional(rollbackFor = Exception.class)
public void addUserRole(Long userId, List<Long> roleIds) {
// ... insert batch ...
throw new RuntimeException("exception occurred");
}Spring rolls back only when an exception propagates out of the transactional method. Catching the exception and not re‑throwing it prevents the rollback.
Fix : Limit try‑catch to the smallest necessary block or let the exception propagate out of the method.
3.3 Incorrect rollbackFor configuration
public void addUser(UserParam param) {
// ...
this.addUserRole(user.getId(), param.getRoleIds());
}
public void addUserRole(Long userId, List<Long> roleIds) throws Exception {
// ... insert batch ...
throw new Exception("exception occurred");
}By default Spring rolls back only for RuntimeException or Error. Throwing a checked Exception does not satisfy the rollback condition, so the transaction remains committed.
Fix : Add rollbackFor = Exception.class (or the specific checked exception) to the @Transactional annotation.
3.4 Using @Transactional on non‑public methods
@Transactional(rollbackFor = Exception.class)
private void addUserRole(Long userId, List<Long> roleIds) {
// ...
throw new RuntimeException("exception occurred");
}CGLIB proxies subclass the target class; private methods are not visible to the subclass and therefore are not intercepted. The transaction advice is never applied.
Fix : Change the method visibility to public.
3.5 Wrong propagation setting
@Transactional(rollbackFor = Exception.class)
public void addUser(UserParam param) {
// insert user
userDAO.insert(user);
try {
userRoleService.addUserRole(user.getId(), param.getRoleIds());
} catch (Exception e) {
log.error(e.getMessage());
}
}The call to addUserRole throws an exception, causing the whole transaction (including the user insert) to roll back.
Fix : Declare propagation = Propagation.REQUIRES_NEW on the role‑adding method so it runs in a separate transaction that can be rolled back independently.
3.6 Long‑running transactions
When a method holds a database connection for the entire duration of time‑consuming operations (e.g., third‑party calls, large batch processing), the connection remains occupied, leading to:
Connection‑pool exhaustion
Potential deadlocks
Long rollback times
Increased replication lag in master‑slave setups
Typical symptoms include CannotGetJdbcConnectionException and UnexpectedRollbackException warnings.
Fix : Split the method into smaller units, isolate non‑transactional logic, and ensure the transaction is open only for the minimal required work.
Conclusion
While @Transactional simplifies transaction handling, developers must be aware of proxy requirements, exception propagation, attribute configuration, method visibility, propagation semantics, and transaction length. Ignoring any of these details can render the transaction ineffective, produce data anomalies, or degrade database performance. Applying the remedies described above ensures that declarative transactions behave as intended.
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.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
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.
