@Transactional in Spring Boot: How It Actually Works (With Code)
The @Transactional
annotation is one of the most powerful — and potentially dangerous — features in Spring Boot. If used properly, it allows for concise and efficient management of database transactions. Used incorrectly, it can silently introduce bugs, data inconsistencies, and performance issues. This post digs into how @Transactional
actually works under the hood, common patterns, rollback caveats, and best practices — all with real code.
1. What Is @Transactional and Why Use It?
In Spring, @Transactional
simplifies transaction management by allowing you to wrap method logic in a transactional context. That means everything inside the method is treated as a single unit of work — either all of it commits or none of it does in case of failure.
Imagine a simple bank application that transfers funds between accounts:
@Service
public class BankService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.debit(amount);
to.credit(amount);
accountRepository.save(from);
accountRepository.save(to);
}
}
Without @Transactional
, if your system crashes after the debit but before the credit, you’ll have inconsistent data. With it, the entire operation is atomic.
2. Propagation and Transaction Boundaries
Spring Boot manages transaction boundaries using proxies. This means transactions are applied only when methods are called from outside the object (via proxy). Be careful with internal method calls — they will bypass the proxy and skip transactional behavior.
Example of a common pitfall:
@Service
public class OrderService {
@Transactional
public void placeOrder() {
saveOrder(); // Internal call — will NOT be transactional!
}
@Transactional
public void saveOrder() {
// DB interaction
}
}
This won’t create a transactional context for saveOrder()
unless invoked from an external class. To fix it, you can extract it to another bean.
@Service
public class OrderDatabaseService {
@Transactional
public void saveOrder(Order order) {
// save to DB
}
}
3. Rollback Rules: What Actually Triggers a Rollback?
By default, Spring only rolls back on unchecked exceptions (i.e., subclasses of RuntimeException
). Checked exceptions do not cause a rollback — unless you explicitly tell it to.
This behavior often surprises developers. Here’s an example:
@Transactional
public void processPayment() throws IOException {
// some database operation
throw new IOException("Network error");
}
Spring will not roll back the transaction because IOException
is a checked exception. If you want it to roll back, specify:
@Transactional(rollbackFor = IOException.class)
public void processPayment() throws IOException {
// database logic
throw new IOException();
}
You can also target multiple exceptions:
@Transactional(rollbackFor = {IOException.class, SQLException.class})
4. Transaction Propagation Strategies
Spring supports different Propagation
settings. These control how transactions should behave when calling transactional methods from another transactional method.
Most common types include:
REQUIRED
(default): Joins the existing transaction or creates a new one.REQUIRES_NEW
: Suspends current transaction and starts a fresh one.NESTED
: Starts a nested transaction that can roll back independently.
Example:
@Transactional
public void mainOperation() {
service.step1(); // Inherited transaction
service.step2(); // Inherited transaction
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void step2() {
// new transaction
}
This is useful when you want step2
to commit or roll back independently of mainOperation()
.
5. Tips, Pitfalls, and Best Practices
Avoid long-running transactions: Holding locks can cause performance bottlenecks and deadlocks. Keep transactional methods short and I/O light.
Don’t use @Transactional on private methods: It won’t be picked up by proxies. Always apply it to public methods in service classes.
Don’t catch exceptions and swallow them: If you catch an exception and don’t re-throw it, Spring won’t know to roll back.
@Transactional
public void process() {
try {
dbCall();
} catch (Exception e) {
// BAD: transaction will still commit
}
}
Test your transaction behavior: Use integration tests to verify rollback behavior using in-memory databases like H2. Spring’s @Transactional
on test classes auto-rolls back after each test.
Use declarative over programmatic transactions: Stick with @Transactional
unless you truly need TransactionTemplate
for advanced control.
Enable transaction management explicitly: Though Spring Boot usually configures it automatically, ensure you have @EnableTransactionManagement
in your config class for clarity.
Conclusion
Understanding how @Transactional
works in Spring Boot can mean the difference between building a robust, enterprise-grade service and chasing elusive bugs in production. Always test edge cases, understand propagation and rollback rules, and don’t blindly scatter @Transactional
everywhere. With great power comes great transactional integrity.
Useful links: