In a @Transactional Spring method, what happens to the transaction if a checked exception is thrown out of the method?
answer
- checked = commit, unchecked = rollback
- rollbackOn: instanceof RuntimeException || Error
- EJB heritage default
- IOException/SQLException commit silently
- opt in with rollbackFor
basics
~10 sBy default Spring COMMITS the transaction. Spring only rolls back automatically on unchecked exceptions (RuntimeException and Error). A checked exception like IOException does not trigger a rollback unless you configure it.
solid answer
~40 sSpring's default rollback rule rolls back only for unchecked exceptions — subclasses of RuntimeException and Error. A checked exception (anything extending Exception but not RuntimeException, e.g. IOException, SQLException, or a custom BusinessException extends Exception) propagates out but the transaction still COMMITS. This surprises developers who assume 'any exception = rollback'. To roll back on a checked exception you must opt in with @Transactional(rollbackFor = ...). This default comes from EJB conventions where checked exceptions modelled recoverable business outcomes and unchecked ones modelled system failures. The practical danger: a method throws a checked exception to signal failure, the caller sees the error, but the partial data was already persisted.
code
java · 18 lines@Service
public class OrderService {
// Custom CHECKED exception (extends Exception, not RuntimeException)
public static class OrderException extends Exception {
public OrderException(String m) { super(m); }
}
@Transactional
public void placeOrder(Order o) throws OrderException {
repository.save(o); // INSERT happens
if (!inventory.reserve(o)) {
// Checked exception -> transaction still COMMITS!
// The save above is NOT rolled back. Surprise.
throw new OrderException("out of stock");
}
}
}go deeper
Must know the one-liner: checked commits, unchecked rolls back, by default.
Should connect it to concrete types (IOException vs NPE) and know rollbackFor fixes it.
Should explain the instanceof RuntimeException||Error mechanism and the data-integrity risk.
Should note the EJB rationale, the Kotlin hierarchy nuance, and design implications for exception strategy.
## The core rule Spring's declarative transaction management (`@Transactional`) wraps your method in a proxy. When the method throws, the proxy consults a **rollback rule** to decide commit vs. rollback. The default rule, implemented in `org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable)`, returns `true` **only** when the throwable is an instance of `RuntimeException` or `Error`. So: - **Unchecked** exceptions — anything extending `RuntimeException` (e.g. `IllegalArgumentException`, `NullPointerException`, Spring's `DataAccessException`) or `Error` — trigger an automatic **rollback**. - **Checked** exceptions — anything extending `Exception` but NOT `RuntimeException` (e.g. `java.io.IOException`, `java.sql.SQLException`, or your own `class OrderException extends Exception`) — do **NOT** trigger rollback. The transaction **commits** even though an exception left the method. ## Why 'checked' vs 'unchecked' matters In Java, `RuntimeException` and its subclasses are *unchecked* (compiler doesn't force a `throws` clause). Everything else under `Exception` is *checked*. Spring's default maps directly onto this hierarchy: it literally does an `instanceof RuntimeException || instanceof Error` test. It is NOT looking at the `throws` keyword — it inspects the runtime type. ## Why the surprise is dangerous Consider a service that does two inserts and then, on a validation failure, throws a checked `BusinessException`. The developer expects both inserts undone. Instead Spring commits them. The caller receives the exception and logs 'failed', but the database now holds half-finished state. This is a classic source of data-integrity bugs. ## The rationale (EJB heritage) Spring inherited this from the EJB spec. The idea: checked exceptions are part of a method's contract and often represent **recoverable business conditions** the caller is expected to handle ("insufficient funds") — not necessarily a reason to discard work. Unchecked exceptions represent **unexpected system faults** — the safe default is to roll back. You can disagree with this default, which is exactly why Spring lets you override it. ## How to fix it (preview) - `@Transactional(rollbackFor = Exception.class)` — roll back on ANY exception, checked included. - `@Transactional(rollbackFor = MyCheckedException.class)` — roll back on a specific checked type. - Or throw/wrap in an unchecked exception instead of a checked one. - Or programmatically: `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()`. ## Kotlin note Kotlin has no *checked* exceptions at the language level, but the JVM type hierarchy still exists. Spring still tests `instanceof RuntimeException`. So a Kotlin exception that extends `Exception` (not `RuntimeException`) will STILL not roll back by default — the rule is about the class hierarchy, not the `throws` keyword.
- Which exception types DOES Spring roll back on by default?Only unchecked ones: any subclass of RuntimeException, plus Error. Everything else (checked exceptions extending Exception directly) commits by default.
- If your method throws a NullPointerException, does the transaction roll back?Yes. NullPointerException extends RuntimeException, so it is unchecked and triggers the default rollback.
saying these in an interview costs you the question
- Claiming any thrown exception always rolls back the transaction
- Thinking Spring looks at the 'throws' clause rather than the runtime type
- Believing checked exceptions roll back and unchecked ones commit (it's the reverse)