@Transactional wraps a method in a database transaction that commits on success
and rolls back on failure. It's simple to add but has sharp edges around
proxying, propagation, and which exceptions trigger a rollback.
The basic contract
Annotate a service method and Spring opens a transaction before it runs, then commits when it returns normally.
@Service
public class AccountService {
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
accounts.debit(from, amount);
accounts.credit(to, amount);
}
}If credit throws, the debit is rolled back — the transfer is all-or-nothing.
Rollback rules surprise people
By default Spring only rolls back on unchecked exceptions. A checked exception commits unless you say otherwise.
@Transactional(rollbackFor = IOException.class)
public void importFile(Path path) throws IOException {
// now an IOException also triggers rollback
}Propagation controls nesting
REQUIRES_NEW suspends the current transaction and runs in its own — useful for
audit logs that must persist even if the outer transaction rolls back.
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String action) {
auditRepo.save(new AuditEntry(action));
}
}The proxy gotcha
Like @Async, @Transactional works through a proxy. Calling a transactional
method from another method in the same class skips the proxy entirely.
Takeaways
- Only unchecked exceptions roll back by default — set
rollbackForotherwise. - Use
REQUIRES_NEWfor work that must commit independently. - Self-invocation bypasses the proxy, so the annotation is ignored.
When in doubt, keep transactions short and scoped to a single service method.