@Transactional in Spring Explained
Fast recall
Quick revision
Core answer
Understanding of declarative transaction management and how Spring proxies enforce transactional boundaries
Not knowing @Transactional only works on public methods called from outside the class because Spring uses proxy-based AOP
Explain the rollback behavior: by default rolls back only on unchecked exceptions (RuntimeException and Error) but not on checked exceptions, and mention you can customize this with rollbackFor attribute
Interview-ready guide
Interview answer
@Transactional removes manual begin, commit, and rollback code around a unit of database work. In the common proxy mode, the caller must invoke the Spring-managed proxy. The interceptor asks the chosen transaction manager to create or join a transaction, calls the method, and then commits or rolls back according to the result and rollback rules. A self-invocation such as this.saveInsideTransaction() does not cross the proxy, so the inner annotation is normally not applied. The default rollback rule covers unchecked exceptions and Error, not every Throwable.
@Transactional in Spring Explained
Direct Answer
@Transactional declares a transaction boundary that Spring usually applies through a proxy. The transaction manager starts or joins a transaction before an intercepted method and commits when it completes normally. By default, RuntimeException and Error trigger rollback; checked exceptions do not unless rollback rules are configured. A call from one method to another on the same object normally bypasses proxy interception.
Code Example
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final InventoryRepository inventoryRepo;
public OrderService(OrderRepository orderRepo, InventoryRepository inventoryRepo) {
this.orderRepo = orderRepo;
this.inventoryRepo = inventoryRepo;
}
@Transactional // Both DB operations in one transaction
public void placeOrder(Long productId, int quantity) {
var inventory = inventoryRepo.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
if (inventory.getStock() < quantity) {
throw new InsufficientStockException(); // Rolls back, no order created
}
inventory.reduceStock(quantity);
inventoryRepo.save(inventory);
var order = new Order(productId, quantity, LocalDateTime.now());
orderRepo.save(order); // If this fails, inventory change is rolled back
}
@Transactional(readOnly = true) // Optimization for read-only queries
public List<Order> getUserOrders(Long userId) {
return orderRepo.findByUserId(userId);
}
@Transactional(rollbackFor = Exception.class) // Roll back on checked exceptions too
public void processPayment(Order order) throws PaymentException {
// Payment logic that might throw checked PaymentException
}
}
Common Mistakes & Pitfalls
- Self-Invocation Problem: Calling a @Transactional method from another method in the same class doesn't start a transaction because you're bypassing the Spring proxy. If methodA calls methodB in the same service, and methodB is @Transactional, no transaction is created. The solution is to inject the service into itself or refactor to call across beans.
- Private Method Annotation: Putting @Transactional on private or protected methods does nothing because Spring uses JDK or CGLIB proxies that only intercept public methods. Spring won't even warn you — the annotation is silently ignored.
- Checked Exception Rollback: Freshers assume all exceptions trigger rollback, but by default only unchecked exceptions do. If you throw IOException or your custom checked exception, the transaction commits anyway unless you add rollbackFor = Exception.class. This causes data inconsistency bugs that are hard to debug.
Check your understanding
Answer these without looking back at the article.
- Why doesn't @Transactional work on private methods?
- What is the self-invocation problem with @Transactional?
- What is transaction propagation in Spring?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.