Avoid the 6 Fatal @Transactional Pitfalls in Spring Boot
This article explains six common and dangerous pitfalls when using Spring's @Transactional annotation, illustrates each with problematic code examples, and provides concrete best‑practice solutions to ensure correct transaction propagation, self‑invocation handling, exception rollback, data source consistency, transaction scope, and method visibility.
Trap 1: Misusing Transaction Propagation
Spring’s Propagation determines how a new transactional method joins or creates a transaction. The default Propagation.REQUIRED joins an existing transaction or creates a new one if none exists. Using REQUIRES_NEW or NESTED incorrectly can cause premature commits or unexpected rollbacks.
@Transactional
public void outerMethod() {
serviceA.operationA(); // part of outer transaction
try {
serviceB.operationB(); // annotated with REQUIRES_NEW
} catch (Exception e) {
log.error("Operation B failed", e);
}
// If an exception is thrown here, operationA rolls back but operationB has already committed!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void operationB() {
// independent transaction
}Understand the semantics of REQUIRED, REQUIRES_NEW and NESTED.
Use REQUIRES_NEW only when a forced commit is truly required.
Prefer NESTED for local rollback when the underlying database supports savepoints.
Trap 2: Self‑Invocation Cancels @Transactional
Spring AOP creates a proxy to apply transaction advice. A call to a @Transactional method from another method in the same class (e.g., this.method()) bypasses the proxy, so the transaction never starts.
@Service
public class UserService {
public void createUser(User user) {
this.insertUser(user); // internal call – transaction ineffective
}
@Transactional
public void insertUser(User user) {
userRepository.save(user);
// Even if an exception is thrown, no rollback occurs
}
}Avoid internal calls to transactional methods.
Extract transactional methods into a separate service and inject that service.
Alternatively, inject the proxy itself:
@Autowired
private ApplicationContext context;
private UserService self;
@PostConstruct
public void init() {
self = context.getBean(UserService.class);
}
public void createUser(User user) {
self.insertUser(user); // transaction works
}For a more robust solution, switch to AspectJ weaving instead of proxy‑based AOP.
Trap 3: Only RuntimeException Triggers Rollback
By default Spring rolls back only for unchecked exceptions ( RuntimeException). Checked exceptions such as IOException or SQLException do not trigger a rollback, which can leave the database in an inconsistent state.
@Transactional
public void processOrder() throws Exception {
updateInventory();
processPayment(); // throws a checked Exception
// Inventory already committed, payment failed → inconsistent data!
}Explicitly declare the exceptions that should cause a rollback, e.g.:
@Transactional(rollbackFor = Exception.class)
public void processOrder() throws Exception {
// business logic
}Or wrap business‑level checked exceptions in a custom unchecked exception.
Trap 4: Non‑Transactional Database Operations Inside a Transaction
Components such as JdbcTemplate or EntityManager normally participate in the current transaction. When a different DataSource or TransactionManager is configured, they may execute outside the transaction, leading to partial commits.
@Transactional
public void updateData() {
myRepository.save(entity); // JPA – participates in transaction
jdbcTemplate.update("UPDATE table SET status = ?", "DONE"); // may be outside transaction
throw new RuntimeException("Oops!"); // possible partial commit
}Ensure all data sources and persistence components share the same TransactionManager.
In multi‑datasource scenarios, explicitly specify the manager:
@Transactional(transactionManager = "txManager")
public void updateData() { ... }Trap 5: Transaction Scope Too Large
A transaction holds a database connection and locks for its entire duration. Including long‑running RPC calls, I/O, or other external operations can keep the connection occupied for seconds, degrading throughput.
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
inventoryService.blockInventory(order.getItems()); // RPC, may take seconds
userService.updateUserOrderStats(order.getUserId());
}Apply the "minimum transaction" principle: wrap only the actual database statements.
Move time‑consuming operations (remote calls, file handling, etc.) outside the @Transactional boundary.
For distributed systems, consider flexible transaction models such as Saga or TCC.
Trap 6: @Transactional on Non‑public Methods Is Ignored
Spring’s proxy‑based AOP intercepts only public methods. Annotations on protected or private methods are ignored, and internal calls to such methods never start a transaction.
@Service
public class AdminService {
@Transactional
protected void adminOperation() {
// not effective
}
public void publicMethod() {
this.adminOperation(); // double trap: non‑public + self‑call
}
}Place @Transactional only on public methods.
If transactional behavior on non‑public methods is required, switch to AspectJ weaving.
Checklist of Best Practices
Choose the correct propagation level; use REQUIRES_NEW sparingly and prefer NESTED when savepoints are supported.
Avoid self‑invocation; delegate transactional work to another Spring bean or inject the proxy.
Specify rollbackFor for checked exceptions or wrap them in unchecked exceptions.
Ensure all persistence components share the same TransactionManager; explicitly set it in multi‑datasource environments.
Keep transactions short: limit them to database operations and move long‑running tasks outside.
Annotate only public methods with @Transactional unless using AspectJ.
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.
Ray's Galactic Tech
Practice together, never alone. We cover programming languages, development tools, learning methods, and pitfall notes. We simplify complex topics, guiding you from beginner to advanced. Weekly practical content—let's grow together!
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.
