By default, which exceptions cause a Spring @Transactional method to roll back, and which let it commit?
answer
- Unchecked + Error -> rollback
- Checked -> commit (EJB legacy)
- DefaultTransactionAttribute.rollbackOn
- Must propagate out of the method
- DataAccessException is unchecked
basics
~10 sBy default Spring rolls back on unchecked exceptions (RuntimeException) and Error. It commits on checked exceptions (any Exception that isn't a RuntimeException). You must opt in to roll back on checked exceptions.
solid answer
~40 sSpring's declarative transactions use a rule: roll back automatically only when a RuntimeException (unchecked) or an Error propagates out of the @Transactional method. If a checked exception (a subclass of Exception that is not a RuntimeException) escapes, Spring commits the transaction. This mirrors EJB's historical convention. It surprises people: throwing a checked exception like IOException or a custom checked business exception commits your work unless you override the rule with rollbackFor. The exception must actually propagate out of the method boundary — if you catch it inside, there's no rollback (and no commit-vs-rollback decision on it at all). To roll back on checked exceptions, use @Transactional(rollbackFor = ...).
code
java · 18 lines@Service
public class TransferService {
// Custom CHECKED exception
public static class BusinessException extends Exception {}
@Transactional
public void transfer() throws BusinessException {
debitAccount(); // partial write
throw new BusinessException(); // checked -> Spring COMMITS the debit!
}
@Transactional
public void charge() {
debitAccount();
throw new IllegalStateException(); // unchecked -> Spring ROLLS BACK
}
}go deeper
Must be able to state the rule crisply: unchecked + Error roll back, checked commit.
Should connect it to the EJB origin and know DataAccessException is unchecked.
Should stress the 'must propagate out' subtlety and the swallowed-exception trap.
Frames it as a design default worth overriding project-wide; may standardize on rollbackFor=Exception.class or RuntimeException-based domain exceptions.
## The default rollback rule When you annotate a method with `@Transactional`, Spring wraps it in a proxy that starts a transaction, runs the method, and then decides to **commit** or **roll back** based on how the method returns. The default decision is: - **Roll back** if the method throws a **`RuntimeException`** (unchecked) or an **`Error`**. - **Commit** if the method returns normally **or** throws a **checked exception** (a subclass of `java.lang.Exception` that is *not* a `RuntimeException`). This is implemented in `DefaultTransactionAttribute.rollbackOn(Throwable ex)`, which returns `ex instanceof RuntimeException || ex instanceof Error`. ### Terminology (define everything) - **Checked exception**: a `Throwable` the compiler forces you to declare or catch — e.g. `IOException`, `SQLException`, or your own `class InsufficientFundsException extends Exception`. - **Unchecked exception**: `RuntimeException` and its subclasses (e.g. `IllegalArgumentException`, `NullPointerException`, Spring's `DataAccessException`) — no compiler enforcement. - **Error**: severe conditions like `OutOfMemoryError`; also triggers rollback by default. ### Why this default? It comes from the **EJB** convention: checked ("application") exceptions were considered *recoverable business outcomes* the caller might handle without abandoning the unit of work, so the container committed; runtime ("system") exceptions signalled a genuine failure, so it rolled back. Spring kept the convention for familiarity. Notably, **all of Spring's own `DataAccessException` hierarchy is unchecked**, so real database failures always trigger rollback by default. ### The most common gotcha A developer writes `class OrderException extends Exception` (checked), throws it mid-transaction after a partial update, and is shocked the partial update is **committed**. The fix is `@Transactional(rollbackFor = OrderException.class)` — or simply extend `RuntimeException` for domain exceptions. ### The exception must escape the method The rollback rule is only evaluated for a `Throwable` that **propagates out of** the transactional method's proxy boundary. If you `try/catch` the exception inside the method and return normally, Spring sees a normal return and commits. If you catch it, do cleanup, and rethrow, the rethrown type is what's evaluated. ### One-line summary > Default = rollback on unchecked (`RuntimeException`) + `Error`, commit on checked. Override with `rollbackFor` / `noRollbackFor`.
- If I catch the RuntimeException inside the @Transactional method and don't rethrow, does Spring roll back?No. The rollback rule is only applied to an exception that propagates out of the transactional method. A swallowed exception means a normal return, so Spring commits. (You could still force it with TransactionAspectSupport.currentTransactionStatus().setRollbackOnly().)
- Are Spring's own data-access exceptions checked or unchecked?Unchecked — the entire org.springframework.dao.DataAccessException hierarchy extends RuntimeException, so genuine DB errors always trigger rollback under the default rule.
saying these in an interview costs you the question
- Claiming Spring rolls back on ALL exceptions by default
- Claiming checked exceptions roll back by default
- Thinking a caught-and-swallowed exception still rolls back automatically
- Believing you must declare rollbackFor to get rollback on NullPointerException