Transaction Management
Intermediate@Transactional wraps method execution in a DB transaction; propagation and isolation attributes control nested-transaction and concurrency behaviour.
Overview
Spring's @Transactional annotation is one of the most important — and most misused — features in the framework. It uses AOP proxying to wrap a method in a database transaction: the transaction begins before the method runs and is committed if the method returns normally, or rolled back if an unchecked exception escapes. Spring Boot auto-configures a PlatformTransactionManager (backed by JPA, JDBC, or a JTA provider) and wires it into the @Transactional infrastructure. The most common source of bugs is misunderstanding the proxy mechanism — calling a @Transactional method from within the same bean (self-invocation) bypasses the proxy entirely, so the transaction is never started.
@Transactional — Core Behaviour
Spring creates a CGLIB subclass proxy around your @Service bean. When an external caller invokes a @Transactional method, the proxy intercepts the call, opens a transaction, delegates to your implementation, then commits or rolls back.
Rollback rules: by default Spring only rolls back on RuntimeException (unchecked) and Error. Checked exceptions do NOT trigger rollback unless you explicitly configure rollbackFor = Exception.class. Always know which exception type your code throws.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
private final InventoryService inventoryService;
// ✅ Transactional — commits when method returns, rolls back on RuntimeException
@Transactional
public Order placeOrder(OrderRequest req) {
Order order = orderRepo.save(new Order(req));
inventoryService.reserve(req.getItems()); // same transaction — any RuntimeException
// here rolls back the whole unit of work
return order;
}
// ✅ Read-only hint — lets the DB / ORM skip dirty-checking & flush
@Transactional(readOnly = true)
public Order getOrder(Long id) {
return orderRepo.findById(id).orElseThrow();
}
// ✅ Also rollback on checked exceptions
@Transactional(rollbackFor = Exception.class)
public void importOrders(List<OrderRequest> orders) throws IOException {
for (OrderRequest req : orders) {
orderRepo.save(new Order(req));
}
}
}Self-Invocation Bug & How to Avoid It
Because @Transactional works through a proxy, calling a @Transactional method from another method in the same class bypasses the proxy — no transaction is started. This is the most common @Transactional bug.
Fix 1 (preferred): restructure the code so the transactional method is called from a different bean. Fix 2: inject self via @Autowired (ugly but works with Spring's default proxy). Fix 3: use @EnableAspectJAutoProxy(exposeProxy = true) and AopContext.currentProxy() (rare).
@Service
public class OrderService {
// ❌ Self-invocation bug — processOrder() is NOT transactional here
public void bulkProcess(List<Long> ids) {
for (Long id : ids) {
processOrder(id); // calls this bean directly, bypasses proxy!
}
}
@Transactional
public void processOrder(Long id) {
// This @Transactional is IGNORED when called from bulkProcess above
}
}
// ✅ Fix — extract to a separate Spring bean
@Service
@RequiredArgsConstructor
public class OrderBulkService {
private final OrderService orderService; // injected proxy
public void bulkProcess(List<Long> ids) {
for (Long id : ids) {
orderService.processOrder(id); // goes through proxy → transaction starts ✅
}
}
}Propagation & Isolation
Propagation controls what happens when a @Transactional method is called from within an existing transaction: • REQUIRED (default) — join the existing tx; create one if none exists. • REQUIRES_NEW — always start a new tx, suspend the outer one (useful for audit logging that must persist even if the outer tx rolls back). • NESTED — savepoint within the outer tx (JDBC only, not JPA).
Isolation sets the SQL isolation level per transaction. The default is DEFAULT (use the DB's configured level, usually REPEATABLE READ for MySQL).
// REQUIRES_NEW — audit log must persist even if outer transaction rolls back
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logEvent(String action, Long userId) {
auditRepo.save(new AuditLog(action, userId, Instant.now()));
// Committed independently — outer rollback does NOT affect this
}
}
// Isolation example — prevent phantom reads
@Transactional(isolation = Isolation.SERIALIZABLE)
public BigDecimal computeBalance(Long accountId) {
return txRepo.sumByAccount(accountId);
}
// application.properties — set default transaction timeout
spring.transaction.default-timeout=30sKey Points to Remember
- 1@Transactional works through a CGLIB proxy — self-invocation (calling a @Transactional method from the same class) bypasses the proxy and starts no transaction.
- 2Default rollback: only RuntimeException and Error. Checked exceptions do NOT roll back unless rollbackFor = Exception.class is configured.
- 3readOnly = true is a performance hint — it skips Hibernate dirty-checking and may enable DB-level optimisations like read replicas.
- 4REQUIRES_NEW suspends the outer transaction and opens a new independent one — useful for audit logs that must survive an outer rollback.
- 5REQUIRED (default propagation) joins an existing transaction; if none exists, it creates one.
- 6A @Transactional annotation on a private method has no effect — Spring proxies only intercept public method calls on injected beans.
Interview Questions
Sign in to ask AriaHow does Spring's @Transactional work internally? What mechanism does it use?
What is the self-invocation problem with @Transactional and how do you fix it?
Does @Transactional roll back on checked exceptions by default? How do you change this?
What is the difference between Propagation.REQUIRED and Propagation.REQUIRES_NEW?
What does @Transactional(readOnly = true) do and when should you use it?
Ask Aria about Transaction Management
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.