I have 13 years of Java experience. I have also interviewed hundreds of developers at MNCs.
Two questions about @Transactional that catch even senior developers. Try answering before you scroll down.
Q1: If a non-transactional method calls a @Transactional method in the same class, does the transaction apply?
@Service
public class OrderService {
public void placeOrder(Order order) {
this.saveOrder(order);
}
@Transactional
public void saveOrder(Order order) {
orderRepository.save(order);
inventoryRepository.update(order);
}
}
Answer: No.
Reason, in plain terms: when you add @Transactional, Spring does not add the transaction logic inside your class. Instead, it creates a second object that sits in front of your class — think of it as a wrapper. Other classes that call saveOrder() (through @Autowired) actually call this wrapper first. The wrapper starts the transaction, then calls your real method, then commits or rolls back.
This wrapper only exists outside your class. So when your own code calls this.saveOrder(order), it skips the wrapper completely and goes straight to the real method. No wrapper means no transaction logic. No rollback if inventoryRepository.update() fails.
This bug never shows up in tests. It only shows up in production, when a failure leaves your data half-updated.
Fix: call the method from a different class, or inject the wrapper of your own class:
@Service
public class OrderService {
@Autowired
private OrderService self; // this is the wrapper, not the raw object
public void placeOrder(Order order) {
self.saveOrder(order); // now it goes through the wrapper
}
@Transactional
public void saveOrder(Order order) {
orderRepository.save(order);
inventoryRepository.update(order);
}
}
Q2: Does @Transactional roll back on every exception?
@Transactional
public void processRefund(Order order) throws RefundException {
paymentRepository.markRefunded(order);
externalRefundService.notify(order); // throws RefundException
}
Answer: No.
Reason: by default, @Transactional only rolls back on unchecked exceptions (RuntimeException and its subclasses). RefundException above is a checked exception. So if it is thrown, Spring lets the transaction commit anyway — including the markRefunded write that happened right before the failure.
Fix: tell Spring to roll back on any exception, not just unchecked ones:
@Transactional(rollbackFor = Exception.class)
public void processRefund(Order order) throws RefundException {
paymentRepository.markRefunded(order);
externalRefundService.notify(order);
}
I cover topics like this in more depth in my guide — Core Java, Java 8 to 21, Multithreading, Spring Boot, Microservices, Design Patterns, and Coding Round Patterns.
Free sample: https://drive.google.com/file/d/1u3PQbTY1gLn34UmWG7Cxx4cmdibD2dvU/view?usp=sharing
Full guide: https://kamaninikhil.gumroad.com/l/java-interview-guide
Got either one right? Drop it in the comments.
Top comments (1)
Great picks — both bite people in prod, not in tests. Two follow-ups interviewers like to push on after these:
On Q1: the self-inject fix works, but reviewers often flag it as a smell. Cleaner options are moving the @Transactional method to a separate bean (so the proxy is actually in the call path), or switching to AspectJ weaving instead of Spring's default proxy-based AOP — with weaving the advice is woven into the bytecode, so self-invocation honors the annotation. Same proxy reason is why @Transactional silently does nothing on private/package-private methods, which is a common gotcha follow-up.
On Q2: worth knowing the 'why' — the default (commit on checked, roll back on unchecked) mirrors the old EJB convention: checked = a business condition you might recover from, unchecked = a system failure. And even with rollbackFor set, if you catch the exception inside the method and don't rethrow, it still commits; conversely a swallowed exception from a nested transactional call can mark the tx rollback-only and blow up the outer one with UnexpectedRollbackException. That last one trips up a lot of seniors.