đ© The Pain Youâve Felt Before
Youâve got a clean Spring Boot service:
@Transactional
public void createOrder() {
saveOrder();
savePayment(); // oops, exception here
}You expect it to roll back, right?
Then you check your databaseâŠ
đ The order table still has data.
đ± The rollback didnât happen.
You restart your app, recheck your config, even blame JPA.
But hereâs the truth â @Transactional didnât fail. You just misunderstood it.
Letâs decode whatâs really happening inside Springâs transaction magic.
âïž What @Transactional Actually Does
When you annotate a method with @Transactional, Spring doesnât âmagicallyâ make rollbacks happen.
It does something more subtle:
It wraps your method inside a proxy, and that proxy manages the transaction lifecycle.
đ§© In plain English:
- Before your method executes â starts a transaction
- If method completes normally â commits
- If method throws a RuntimeException â rolls back
Sounds simple, right?
Well⊠hereâs where most developers get trapped.
đŁ The 7 Deadly @Transactional Misconceptions
1ïžâŁ Youâre Calling a Transactional Method from the Same Class
The #1 rookie mistake â self-invocation.
@Service
public class OrderService {
@Transactional
public void placeOrder() {
saveOrder();
notifyCustomer();
} public void notifyCustomer() {
sendEmail();
throw new RuntimeException("Email failed!");
}
}Hereâs the catch đ
placeOrder() calls notifyCustomer() inside the same class.
But Spring creates transactions through proxies â and internal method calls donât go through the proxy.
đ„ Result: @Transactional is ignored completely.
â
Fix: Move notifyCustomer() to another bean, or call via AopContext:
((OrderService) AopContext.currentProxy()).notifyCustomer();2ïžâŁ Youâre Throwing the Wrong Exception
By default, Spring only rolls back for unchecked exceptions (i.e. subclasses of RuntimeException).
So this wonât roll back đ
@Transactional
public void process() throws Exception {
throw new Exception("Checked exception");
}â
Fix:
Specify rollback rules manually:
@Transactional(rollbackFor = Exception.class)
public void process() throws Exception {
throw new Exception("Now it rolls back!");
}đĄ Rule of thumb:
- Runtime exceptions â rollback automatically
- Checked exceptions â only rollback if you tell Spring to
3ïžâŁ Your Transactional Method Isnât public
This oneâs sneaky.
Springâs AOP proxies only intercept public methods by default.
So if youâve written:
@Transactional
private void doWork() { ... }âŠit silently does nothing.
Get Dollyâs stories in your inbox
Join Medium for free to get updates from this writer.
â
Fix: Always make transactional methods public.
4ïžâŁ Youâre Mixing Different Transaction Managers
If youâre using multiple datasources (say one MySQL + one MongoDB) and you annotate:
@Transactional
public void syncData() {
mysqlRepo.save(...);
mongoRepo.save(...);
}Only one of them actually participates in the transaction â the other wonât roll back.
â Fix: Specify your transaction manager:
@Transactional("mysqlTransactionManager")Or use a distributed transaction manager (like Atomikos or Narayana).
5ïžâŁ Youâre Using REQUIRES_NEW or NESTED Incorrectly
Spring supports propagation behaviors, like:
REQUIREDâ joins existing transaction (default)REQUIRES_NEWâ always starts a new transactionNESTEDâ creates savepoints
Example:
@Transactional
public void createOrder() {
saveOrder();
try {
saveAudit();
} catch (Exception e) {
log.error("Audit failed");
}
}@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAudit() {
throw new RuntimeException("Audit failed");
}Result:
saveAudit()rolls back its own transaction.- But main transaction (
createOrder) still commits.
đĄ Lesson: Propagation settings control rollback boundaries â use carefully.
6ïžâŁ Youâre Not Using the Right PlatformTransactionManager
If youâre using JPA, make sure your config uses the JpaTransactionManager, not DataSourceTransactionManager.
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}Otherwise, Hibernate flush behavior might be inconsistent â partial commits, no rollback, and ghost data. đ»
7ïžâŁ Your Method Is Not in a Spring-Managed Bean
Spring transactions only work when the object is managed by Springâs container.
So if youâre manually creating objects like:
OrderService service = new OrderService();
service.createOrder();Spring canât intercept that call â no transaction.
â
Fix: Always inject via Spring (@Autowired, constructor, or bean).
đŹ Bonus Tip: Verify Itâs Working
You can always check the transaction status programmatically:
TransactionStatus status =
TransactionAspectSupport.currentTransactionStatus();
System.out.println(status.isNewTransaction());Or enable debug logs in application.yml:
logging:
level:
org.springframework.transaction: TRACEYouâll literally see when transactions start, commit, or roll back.
đ§© TL;DR â The Transaction Truth Table
đ§ Final Thoughts
@Transactional isnât broken â itâs just honest.
It works exactly as designed, but only if you understand how Spring AOP and propagation work together.
The next time your rollback doesnât happen, donât panic â check the rules above. Chances are, itâs not a bug. Itâs a proxy. đ
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We donât receive any funding, we do this to support the community. â€ïž
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, Instagram. You can also subscribe to our weekly newsletter.
And before you go, donât forget to clap and follow the writerïž!










