Which Exceptions Trigger Rollback by Default
In Spring Framework, a transaction does not automatically roll back for every thrown exception. There is a strict architectural divide:
🟢 Junior Level
In Spring Framework, a transaction does not automatically roll back for every thrown exception. There is a strict architectural divide:
The Default Rule in 30 Seconds
Spring automatically triggers a ROLLBACK strictly upon encountering Unchecked Exceptions:
java.lang.RuntimeExceptionand any of its subclasses (NullPointerException,IllegalArgumentException,IllegalStateException,DataAccessException, etc.).java.lang.Errorand any of its subclasses (OutOfMemoryError,StackOverflowError, etc.).
All Checked Exceptions (direct subclasses of java.lang.Exception, such as IOException, SQLException, ClassNotFoundException, or custom business exceptions declared as extends Exception) DO NOT trigger a rollback by default — Spring executes a COMMIT!
Exception escapes @Transactional method:
│
▼
Is the thrown object an
instance of RuntimeException or Error?
/ \
YES NO (Checked Exception)
/ \
[ ROLLBACK ] [ COMMIT ]
(Mutations undone) (Data saved permanently!)
Practical Demonstration of Default Behavior
@Service
public class DefaultRollbackService {
private final AccountRepository accountRepository;
public DefaultRollbackService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
// SCENARIO 1: RuntimeException -> Automatic ROLLBACK
@Transactional
public void debitWithRuntimeException(Long id, BigDecimal amount) {
Account account = accountRepository.findById(id).orElseThrow();
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
// Unchecked exception: Spring catches this and executes connection.rollback()!
throw new IllegalStateException("Unexpected technical failure");
}
// SCENARIO 2: Checked Exception -> Silent COMMIT (PRODUCTION HAZARD)
@Transactional
public void debitWithCheckedException(Long id, BigDecimal amount) throws IOException {
Account account = accountRepository.findById(id).orElseThrow();
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);
// Checked exception: Spring considers this normal business flow and COMMITS!
// The money debit is permanently committed to the database despite the error!
throw new IOException("Failed to write invoice file to network disk");
}
}
🟡 Middle Level
Historical Philosophy: Why Spring Designed It This Way
This behavioral model was established by Rod Johnson in Spring 1.0, inherited from the EJB (Enterprise Java Beans) specification:
RuntimeExceptionrepresents an unforeseen technical crash or programming flaw (code bug, null pointer, database driver crash). The application state is compromised and cannot continue safely $\rightarrow$ an unconditionalROLLBACKis required.- Checked
Exceptionwas designed into Java as an anticipated alternative business outcome (e.g.,InsufficientFundsExceptionorUserAlreadyExistsException). The compiler forces the calling code to handle the outcome, and Spring assumes the application may intend to preserve preceding state mutations (such as incrementing failed-attempt audit counters) $\rightarrow$ aCOMMITis executed.
[!NOTE] In modern Java engineering, checked exceptions are widely considered obsolete, and modern libraries rely almost exclusively on unchecked exceptions. However, Spring’s
@Transactionaldefaults remain unchanged to guarantee strict backwards compatibility.
Exception Classification and Default Spring Response
| Exception Class | Java Classification | Default Spring Action | Database Outcome |
|---|---|---|---|
NullPointerException |
RuntimeException |
❌ ROLLBACK | Modifications completely reverted |
IllegalArgumentException |
RuntimeException |
❌ ROLLBACK | Modifications completely reverted |
DataIntegrityViolationException |
RuntimeException |
❌ ROLLBACK | Modifications completely reverted |
OutOfMemoryError |
Error |
❌ ROLLBACK | Reverted (if JVM remains responsive) |
IOException |
Checked Exception |
⚠️ COMMIT | Data permanently saved to DB! |
SQLException |
Checked Exception |
⚠️ See below | Translated to RuntimeException |
CustomException extends Exception |
Checked Exception |
⚠️ COMMIT | Data permanently saved to DB! |
Why Database Errors (SQLException) Still Trigger Rollbacks
Because raw JDBC java.sql.SQLException is a checked exception, why do database constraint violations (duplicate keys, deadlocks) still trigger transaction rollbacks?
- Spring registers a Persistence Exception Translation mechanism (
PersistenceExceptionTranslationPostProcessorand@Repository). - Spring intercepts raw
SQLExceptioninstances and translates them into its unifiedorg.springframework.dao.DataAccessExceptionhierarchy (DuplicateKeyException,CannotAcquireLockException). - Because
DataAccessExceptionextendsRuntimeException, it triggers Spring’s standard default rollback policy.
🔴 Senior Level
Kernel Internals: RuleBasedTransactionAttribute
The evaluation of whether an exception requires rollback is encapsulated inside RuleBasedTransactionAttribute.rollbackOn():
// Spring Framework source logic: org.springframework.transaction.interceptor.RuleBasedTransactionAttribute
@Override
public boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
// 1. Search for the most specific matching custom rule:
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
// If a custom rule matched, its decision takes precedence:
if (winner != null) {
return !(winner instanceof NoRollbackRuleAttribute);
}
// 2. FALLBACK DEFAULT RULE (if no custom rules matched):
return (ex instanceof RuntimeException || ex instanceof Error);
}
The Inheritance Distance Algorithm (getDepth) for Conflicting Rules
When an annotation declares conflicting rollbackFor and noRollbackFor rules, Spring calculates class inheritance distance:
@Transactional(
rollbackFor = Exception.class, // Depth for NullPointerException = 2 (NPE -> RuntimeException -> Exception)
noRollbackFor = RuntimeException.class // Depth for NullPointerException = 1 (NPE -> RuntimeException)
)
public void processOrder() {
throw new NullPointerException();
}
Outcome: Method getDepth() returns depth 1 for the noRollbackFor rule and depth 2 for rollbackFor. The rule with the smallest inheritance distance (closest ancestor) wins. The transaction commits!
The Silent Hazard of String-Based Rules (rollbackForClassName)
Spring allows configuring exception rules using string class names:
@Transactional(rollbackForClassName = "Com.MyCompany.BusinesException") // TYPO!
- Spring does not validate string class names at compilation time or application startup (it uses lazy pattern matching against the exception’s FQCN).
- If a developer introduces a typographical error, the rule will never match (
depth == -1). - When the checked exception is thrown at runtime, Spring silently falls back to its default policy and commits the transaction!
- Senior Best Practice: Always use type-safe class literals:
rollbackFor = BusinessException.class.
4 Tricky Questions
1. What happens if a checked exception (IOException) is wrapped inside a CompletionException or UndeclaredThrowableException?
Answer:
The transaction rolls back (ROLLBACK), even if rollbackFor was omitted!
Spring AOP proxies inspect the top-level exception that physically escapes the method invocation. Both CompletionException (from CompletableFuture) and UndeclaredThrowableException (from dynamic proxy reflections) inherit directly from RuntimeException. Because the top-level exception satisfies ex instanceof RuntimeException == true, Spring unconditionally triggers a transaction rollback.
2. How is a rule conflict resolved if a method specifies rollbackFor = Exception.class and noRollbackFor = IllegalArgumentException.class, and throws a NumberFormatException?
Answer:
The transaction COMMITS!
The RuleBasedTransactionAttribute algorithm calculates the inheritance depth to the nearest ancestor:
NumberFormatExceptiondirectly extendsIllegalArgumentException(distance = 1 innoRollbackFor).NumberFormatExceptionextendsIllegalArgumentException$\rightarrow$RuntimeException$\rightarrow$Exception(distance = 3 inrollbackFor). Spring selects the rule with the minimal depth (depth = 1). Because the winning rule is aNoRollbackRuleAttribute, rollback is suppressed and Spring commits the transaction.
3. What happens if a service method catches a RuntimeException in a try-catch block, logs the error, and returns normally?
Answer: A Silent Commit occurs! Spring’s AOP proxy intercepts exceptions only when they propagate across the method boundary. If the exception is caught and swallowed internally:
TransactionInterceptornever learns that a failure occurred.- The proxy concludes that the business method executed successfully.
- The interceptor invokes
connection.commit(). Any database modifications performed prior to the error are permanently saved, potentially violating domain invariants. To trigger a rollback without re-throwing, you must invokeTransactionAspectSupport.currentTransactionStatus().setRollbackOnly().
4. Why is OutOfMemoryError included in the default rollback condition, and is a physical database rollback guaranteed when it occurs?
Answer:
Error is included in the default check (ex instanceof Error) because it signals catastrophic JVM instability where committing unverified partial writes is dangerous.
However, a physical database rollback is NOT guaranteed:
- When
OutOfMemoryErroroccurs, the JVM heap is severely exhausted. - Spring’s attempt to execute
connection.rollback()requires allocating new objects in the heap (JDBC driver state, network socket buffers). If memory is completely depleted, the rollback invocation itself throws a secondaryOutOfMemoryError. - In this scenario, the TCP connection between the application and the database server abruptly terminates. Data integrity is ultimately preserved by the database engine itself: the database server detects the dropped connection and automatically aborts and rolls back the orphaned transaction via its WAL/Undo logs.
🎯 Interview Cheat Sheet
- Default Rollback Policy: Spring triggers
ROLLBACKonRuntimeExceptionandError. - Default Commit Policy: Spring triggers
COMMITon checked exceptions (extends Exception). - All-Exception Rollback: Always configure
@Transactional(rollbackFor = Exception.class)on production write services. - Rule Conflict Resolution: When multiple rules match, Spring chooses the rule with the smallest inheritance distance (
getDepth()). - Spring Data Exception Translation: Low-level checked
SQLExceptionis automatically translated into uncheckedDataAccessException, ensuring automatic rollback. - Swallowed Exceptions: Catching and ignoring a
RuntimeExceptionleads to a silentCOMMIT. - String Rule Danger: Avoid
rollbackForClassName; typographical errors fail silently and cause unexpected commits.