Walk through the internal mechanism that decides commit vs. rollback in Spring, and why a checked exception commits by default.
answer
- TransactionInterceptor -> completeTransactionAfterThrowing
- DefaultTransactionAttribute.rollbackOn = instanceof RuntimeException||Error
- RuleBasedTransactionAttribute + getDepth
- shallowest depth wins, else super()
- commit-then-rethrow when false
basics
~10 sThe transaction interceptor catches the thrown exception and calls rollbackOn(). The default (DefaultTransactionAttribute) returns true only for RuntimeException and Error, so checked exceptions fall through to commit. rollbackFor adds extra RollbackRuleAttribute entries.
solid answer
~40 sDeclarative transactions run through TransactionInterceptor (an AOP advice). It invokes your method inside try/catch. On a throwable it calls TransactionAspectSupport.completeTransactionAfterThrowing, which consults the TransactionAttribute's rollbackOn(ex). The base DefaultTransactionAttribute.rollbackOn returns true only if ex is a RuntimeException or Error — that's why checked exceptions commit. When you specify rollbackFor/noRollbackFor, Spring uses RuleBasedTransactionAttribute holding a list of RollbackRuleAttribute / NoRollbackRuleAttribute. Its rollbackOn walks the exception's superclass chain computing each rule's 'depth' (distance to a matching type) and the shallowest-depth rule wins; if none matches it falls back to the super (RuntimeException/Error) behaviour. So a broad rollbackFor=Exception.class plus a specific noRollbackFor=SubType.class resolves correctly by proximity. Rollback only fires for exceptions that actually escape the proxied method.
code
java · 21 lines// Effective logic Spring runs (simplified from Spring source):
// DefaultTransactionAttribute
boolean rollbackOn(Throwable ex) {
return ex instanceof RuntimeException || ex instanceof Error;
}
// RuleBasedTransactionAttribute
boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
for (RollbackRuleAttribute rule : rollbackRules) {
int depth = rule.getDepth(ex); // -1 if no match
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
if (winner == null) return super.rollbackOn(ex); // fall back to default
return !(winner instanceof NoRollbackRuleAttribute);
}go deeper
Not expected to know internals; just the outcome.
Should know rollbackOn exists and the RuntimeException/Error test.
Should trace TransactionInterceptor -> rollbackOn and describe depth-based rule resolution.
Should reason about rule composition, self-invocation/proxy limits, and UnexpectedRollbackException interplay.
## The call chain 1. A call to a `@Transactional` bean goes through a proxy (`TransactionInterceptor`, an AOP `MethodInterceptor`). 2. The interceptor starts/joins a transaction, then invokes the target method inside a `try`. 3. If the method throws, `TransactionAspectSupport.completeTransactionAfterThrowing(txInfo, ex)` runs. 4. It asks `txInfo.transactionAttribute.rollbackOn(ex)`: - `true` -> `transactionManager.rollback(status)` - `false` -> `transactionManager.commit(status)` (the exception still propagates to the caller — commit, then rethrow) ## The default rule — `DefaultTransactionAttribute.rollbackOn` ```java public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); } ``` That single line is the whole reason checked exceptions commit. It is a **runtime type test**, not a compile-time `throws` analysis. Any `Exception` that is not a `RuntimeException` returns `false` -> commit. ## The rule-based rule — `RuleBasedTransactionAttribute` When you set `rollbackFor`/`noRollbackFor`, Spring builds a `RuleBasedTransactionAttribute` carrying a list of `RollbackRuleAttribute` and `NoRollbackRuleAttribute`. Its `rollbackOn`: 1. Iterates all rules, calling `rule.getDepth(ex)` — the number of superclass hops from the thrown exception's class up to the rule's target type, or `-1` if no match. 2. Keeps the rule with the **smallest non-negative depth** (closest ancestor). 3. If the winning rule is a `NoRollbackRuleAttribute`, do NOT roll back; if it's a `RollbackRuleAttribute`, roll back. 4. If **no** rule matches, it defers to `super.rollbackOn(ex)` — i.e. the default RuntimeException/Error behaviour. This proximity algorithm is why you can combine broad and narrow rules: ```java @Transactional(rollbackFor = Exception.class, noRollbackFor = ReportableWarning.class) ``` For a `ReportableWarning`, the `noRollbackFor` rule has depth 0 (exact) vs the `Exception.class` rule's larger depth, so no-rollback wins. ## Why the default is what it is (EJB / design philosophy) The convention descends from EJB CMT. Checked exceptions are part of a method's declared contract and were intended for **anticipated, recoverable business conditions** the caller must handle — throwing one didn't necessarily mean 'discard the work'. Unchecked exceptions signalled **programming errors or unrecoverable system faults**, where discarding the unit of work is the safe default. Spring adopted the same split so its behaviour would be least surprising to EJB veterans and to keep a clean 'system fault vs business signal' distinction. ## Consequences / edge cases - **Swallowed exceptions never trigger the rule.** If the exception doesn't leave the proxied method, `completeTransactionAfterThrowing` is never called. - **Self-invocation bypasses the proxy.** Calling a `@Transactional` method from within the same bean skips the interceptor entirely, so no rule runs. - **`commit` can still fail.** Even when `rollbackOn` returns `false`, the physical commit may fail (constraint violation) and surface as an exception; and a nested inner tx that set rollback-only can cause an `UnexpectedRollbackException` at the outer commit. - **Error is included** in the default — an `OutOfMemoryError` rolls back, which is rarely what people picture but is correct.
- With rollbackFor = Exception.class and noRollbackFor = IllegalStateException.class, what happens when IllegalStateException is thrown?The noRollbackFor rule matches at depth 0 (exact), which is shallower than Exception.class, so no-rollback wins and the transaction commits — even though IllegalStateException is normally an auto-rollback unchecked exception.
- Does the exception still reach the caller when rollbackOn returns false?Yes. Spring commits the transaction and then rethrows the original exception; the caller still sees it. Commit vs rollback is independent of whether the exception propagates.
saying these in an interview costs you the question
- Saying Spring inspects the method's throws clause at compile time
- Claiming the first matching rule wins rather than the closest (shallowest) match
- Believing a false rollbackOn swallows the exception (it still propagates)