@Transactional
A transaction groups several database operations into a single atomic unit: they all succeed or all roll back. Spring’s @Transactional (from org.springframework.transaction.annotation) turns this into a single declarative annotation, wrapping your method in a transaction managed by a proxy. Used well it guarantees consistency; used carelessly it silently does nothing. This page covers where to place it, how propagation and isolation behave, the default rollback rules, and the self-invocation trap that catches almost everyone.
Where to Put @Transactional
Put @Transactional on the service layer, not on repositories or controllers.
- Repositories already run each method in a transaction, but they are too fine-grained — a business operation usually spans multiple repository calls that must commit together.
- Controllers should stay free of persistence concerns; keeping transactions out of the web layer also avoids holding a database connection while serializing the response.
- The service is where a business use case lives, so it is the natural transaction boundary.
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class TransferService {
private final AccountRepository accountRepository;
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
from.withdraw(amount);
to.deposit(amount);
// both updates commit together, or neither does
}
}
Tip: Use constructor injection with
@RequiredArgsConstructorso the bean is immutable and easy to test. See dependency injection for how Spring wires this proxy-backed bean.
Propagation
Propagation decides how a method joins or creates a transaction relative to one that may already be running. The default, REQUIRED, covers most cases.
| Propagation | If a transaction exists | If none exists |
|---|---|---|
REQUIRED | join it (default) | create a new one |
REQUIRES_NEW | suspend it, start a new | create a new one |
SUPPORTS | join it | run non-transactional |
MANDATORY | join it | throw an exception |
NESTED | nested savepoint | create a new one |
NEVER | throw an exception | run non-transactional |
NOT_SUPPORTED | suspend it, run without | run non-transactional |
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog(AuditEntry entry) {
auditRepository.save(entry); // commits even if the caller later rolls back
}
Note:
REQUIRES_NEWis ideal for audit logs or notifications that must persist regardless of the outer transaction’s outcome.
Isolation Levels
Isolation controls how concurrent transactions see each other’s uncommitted changes, trading consistency against throughput.
DEFAULT— use the database’s own default (usuallyREAD_COMMITTEDon PostgreSQL/MySQL InnoDB).READ_COMMITTED— sees only committed data; prevents dirty reads.REPEATABLE_READ— re-reads return the same rows; prevents non-repeatable reads.SERIALIZABLE— full isolation, as if transactions ran one at a time; strongest and slowest.
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Report buildReport() { ... }
readOnly Optimization
Mark query-only methods with readOnly = true. Hibernate then sets the flush mode to MANUAL, skipping dirty-checking and snapshot tracking, and the driver/database can apply read-path optimizations.
@Transactional(readOnly = true)
public List<AccountView> listAccounts() {
return accountRepository.findAllProjectedBy();
}
Tip: Make every read-only service method
readOnly = true. It is a cheap, safe performance win and documents intent.
Rollback Rules
By default Spring rolls back only on RuntimeException and Error — not on checked exceptions. A checked exception escaping a @Transactional method commits the transaction.
@Transactional
public void process() throws IOException {
repo.save(entity);
throw new IOException("boom"); // checked -> transaction STILL COMMITS
}
Override the behavior with rollbackFor / noRollbackFor:
@Transactional(rollbackFor = IOException.class)
public void process() throws IOException { ... } // now rolls back
@Transactional(noRollbackFor = ValidationException.class)
public void save() { ... } // commit despite this runtime exception
Warning: Catching an exception inside the method and not rethrowing it means Spring never sees it — the transaction commits. If you handle and want a rollback anyway, call
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(). For exception design, see Java exceptions.
The Self-Invocation Pitfall
@Transactional works through a Spring AOP proxy: Spring wraps your bean in a proxy that opens the transaction before delegating to your real method. The proxy only intercepts calls that arrive through it — i.e. calls from other beans.
When a method calls another @Transactional method on the same bean (this.other()), the call goes straight to the target object and bypasses the proxy entirely, so the annotation is silently ignored.
@Service
@RequiredArgsConstructor
public class OrderService {
@Transactional
public void placeOrder(Order order) {
saveAudit(order); // self-invocation -> @Transactional below is IGNORED
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAudit(Order order) { // never runs in its own transaction here
auditRepository.save(new AuditEntry(order));
}
}
How to fix it
- Move the method to a separate bean and inject it, so the call crosses the proxy boundary (the cleanest fix).
- Self-injection — inject the bean into itself and call through that reference:
@Service
@RequiredArgsConstructor
public class OrderService {
private final ApplicationContext context;
@Transactional
public void placeOrder(Order order) {
context.getBean(OrderService.class).saveAudit(order); // goes through the proxy
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAudit(Order order) { ... }
}
Understanding why this happens requires the bean/proxy model explained in dependency injection.