Transactions
Intermediate@Transactional wraps a method in a database transaction — all operations commit together or roll back together. Understanding propagation and isolation levels prevents subtle data corruption bugs in production.
Overview
Spring's transaction management is declarative — @Transactional on a service method causes a proxy to begin a transaction before the method and commit or roll back after. By default, Spring rolls back on unchecked exceptions (RuntimeException and Error) but NOT on checked exceptions. Propagation controls what happens when a @Transactional method calls another — REQUIRED (default) joins the existing transaction, REQUIRES_NEW suspends it and starts a fresh one. Isolation level controls what concurrent transactions can see.
@Transactional Basics and Rollback Rules
Apply @Transactional at the service layer, not the repository or controller. The proxy cannot intercept internal method calls (self-invocation), so calling a @Transactional method from another method in the same class does NOT start a transaction.
@Service
@Transactional // class-level default: all public methods are transactional
public class OrderService {
// Inherits class-level @Transactional
public Order createOrder(CreateOrderRequest req) {
Order order = orderRepository.save(new Order(req));
inventoryService.reserve(req.items()); // same transaction
paymentService.charge(req.paymentMethod(), order.total()); // same transaction
// If paymentService.charge() throws → entire transaction rolls back
return order;
}
// Override: read-only hint (enables flush-mode optimization)
@Transactional(readOnly = true)
public Order findById(String id) {
return orderRepository.findById(id).orElseThrow();
}
// Roll back on checked exception too
@Transactional(rollbackFor = PaymentException.class)
public void processRefund(String orderId) throws PaymentException {
// ...
}
// ❌ Self-invocation — proxy is bypassed, NO transaction started
public void methodA() {
this.methodB(); // calls target, not proxy — @Transactional ignored!
}
@Transactional
public void methodB() { /* ... */ }
}Propagation Levels
Propagation defines how a transaction behaves when one @Transactional method calls another. REQUIRED (default) is safe for most cases. REQUIRES_NEW is critical when you need an operation to commit independently — like audit logging that must persist even if the outer transaction rolls back.
@Service
public class AuditService {
// REQUIRES_NEW — suspends outer transaction, commits independently
// Use for: audit logs, notifications, anything that must NOT rollback
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String userId, String action) {
auditRepository.save(new AuditLog(userId, action, Instant.now()));
// This ALWAYS commits, even if the caller's transaction rolls back
}
}
@Service
public class PaymentService {
private final AuditService auditService;
@Transactional
public void charge(String userId, BigDecimal amount) {
// outer transaction T1
processCharge(userId, amount);
auditService.logAction(userId, "PAYMENT_CHARGED"); // T2 — new transaction
if (someCondition) {
throw new RuntimeException(); // T1 rolls back, T2 already committed
}
}
}
// Propagation summary:
// REQUIRED — join existing; create new if none (default)
// REQUIRES_NEW — always new transaction; suspend existing
// SUPPORTS — join if exists; non-transactional if none
// NOT_SUPPORTED — run non-transactionally; suspend existing
// MANDATORY — must have existing transaction; throw if none
// NEVER — must NOT have transaction; throw if one exists
// NESTED — nested savepoint within existing (rollback to savepoint)Isolation Levels
Isolation controls what dirty data from concurrent transactions a transaction can read. Higher isolation = fewer anomalies but more locking and lower throughput. Choose the minimum isolation level that prevents the anomalies you care about.
// Isolation levels and anomalies they prevent:
// READ_UNCOMMITTED — can read dirty (uncommitted) data. No locks. Fastest.
// READ_COMMITTED — can't read dirty data. Default for PostgreSQL/Oracle.
// REPEATABLE_READ — repeated reads of same row return same value. Default for MySQL.
// SERIALIZABLE — full isolation. Slowest. Transactions execute as if sequential.
// Anomalies:
// Dirty Read — reading uncommitted data from another transaction
// Non-repeatable — same row reads different values within one transaction
// Phantom Read — range query returns different rows on repeated reads
@Transactional(isolation = Isolation.REPEATABLE_READ)
public OrderSummary generateReport(String customerId) {
// First read — 5 orders
List<Order> orders = orderRepository.findByCustomerId(customerId);
BigDecimal total = orders.stream().map(Order::getTotal).reduce(ZERO, BigDecimal::add);
// ... heavy processing ...
// Second read of same data guaranteed to return same rows (no phantom)
return new OrderSummary(orders.size(), total);
}
// For most web apps: READ_COMMITTED is sufficient
// Use SERIALIZABLE only for financial ledger / inventory reservation
// Optimistic locking (@Version) is often better than higher isolationKey Points to Remember
- 1@Transactional on a class applies to all public methods — methods can override with their own annotation.
- 2Spring rolls back on RuntimeException/Error by default — use rollbackFor for checked exceptions.
- 3Self-invocation bypasses the proxy — internal @Transactional calls from the same class have no effect.
- 4readOnly = true is an optimization hint to the database — use it for all read-only service methods.
- 5REQUIRES_NEW suspends the outer transaction and commits independently — essential for audit logs.
- 6Prefer READ_COMMITTED isolation + optimistic locking (@Version) over high isolation levels in most web apps.
Interview Questions
Sign in to ask AriaWhy does @Transactional not work on private methods or self-invocation?
What is the default rollback behaviour of @Transactional?
What is the difference between REQUIRED and REQUIRES_NEW propagation?
What isolation level does PostgreSQL use by default and what anomalies does it prevent?
How does optimistic locking with @Version differ from database-level isolation?
Ask Aria about Transactions
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.