@Transactional: Propagation, Isolation, and Rollback Rules
- Last updated
- Prerequisites:
- Spring dependency injection basics
- Understanding of database transactions (ACID)
- spring
- spring-boot
- transactional
- propagation
- isolation
- rollback
- jdbc
Read at your depth
The practical view
@Transactional on a method makes Spring manage a transaction around it: it begins before the method, commits after success, and rolls back on failure. @Transactional(readOnly = true) is for queries. Propagation controls how it interacts with an existing transaction: REQUIRED (default — join or create), REQUIRES_NEW (suspend and create a new one), NESTED (savepoint), MANDATORY (must exist). Isolation sets the database isolation level (READ_COMMITTED, etc.). Rollback rules: by default, rollback only on RuntimeException/Error, NOT checked exceptions — configure with rollbackFor = Exception.class.
The same idea in other frameworks
N/A — server-side Spring concern.N/A — Spring concern.N/A — Spring concern.N/A — Spring concern.Legacy vs modern
No transaction (per-statement autocommit) vs @Transactional
Without @Transactional each statement commits separately (partial writes on failure); the annotation makes the whole method one atomic unit.
void transfer(String from, String to, int amount) {
accountRepo.debit(from, amount); // ❌ OLD: commits immediately
accountRepo.credit(to, amount); // ❌ OLD: if this fails, debit stays
}@Transactional
void transfer(String from, String to, int amount) { // ✅ NEW: one atomic unit
accountRepo.debit(from, amount);
accountRepo.credit(to, amount);
}Interview gotchas
Context
This is the classic Spring transaction gotcha — interviewers want the proxy model explanation, because self-invocation silently skips the transaction interceptor.
The mechanical answer
@Transactional works through a proxy: Spring wraps the bean, and calls THROUGH the proxy get intercepted. When a method inside the same class calls another @Transactional method directly (this.someMethod()), the call goes to the raw target object, not through the proxy — so the transaction interceptor never runs, and the inner @Transactional is ignored (its transaction is not started/committed separately). This is why self-invocation breaks transactions, @Async, and other AOP-based annotations. Fixes: inject the bean into itself (self-injection to get the proxy), extract the method to another bean, use TransactionTemplate programmatically, or restructure so the call goes through the proxy. The transaction boundary is at the proxy, not the method.
Trap
The naive answer is 'Spring creates a transaction for every @Transactional method'. Only calls that pass through the proxy get intercepted. Another trap: assuming an inner call 'joins' the outer transaction automatically — it only does if it goes through the proxy AND the propagation is REQUIRED; self-invocation skips it entirely. Candidates who mention the proxy and self-injection fix show real Spring experience.
Context
This tests the rollback rule semantics — interviewers want the default behavior rationale and the rollbackFor/noRollbackFor configuration that production code needs.
The mechanical answer
By default, the transaction interceptor rolls back only on unchecked exceptions (RuntimeException and Error), not checked exceptions (Exception subclasses). The rationale is a convention: checked exceptions typically represent expected, recoverable business conditions (e.g. 'insufficient funds'), for which the business code intends to handle and continue — auto-rolling back would be surprising. Unchecked exceptions represent programming/system errors where the transaction state is unreliable. To override, use rollbackFor = SomeCheckedException.class (roll back on that checked exception) and noRollbackFor = SomeRuntimeException.class (don't roll back on that unchecked one). Production code frequently needs rollbackFor = Exception.class to treat all failures as transactional failures.
Trap
The naive answer is 'Spring rolls back on any exception'. It rolls back only RuntimeException/Error by default — a checked exception leaves the transaction to be committed (if nothing else failed). Another trap: throwing an exception after a repository write and expecting automatic rollback — if it's a checked exception and you didn't set rollbackFor, the partial write COMMITS. Candidates who can state the default and the override attributes show real transactional knowledge.