The default rollback rule trips almost everyone: Spring rolls back only on RuntimeException (unchecked) and Error — a checked exception commits the transaction. This is a deliberate inheritance from EJB semantics, and it's the source of the classic "my method threw but the data was still saved" bug.
The default rule
When the proxied method throws, the TransactionInterceptor checks the exception type:
RuntimeExceptionorError→ roll back.- any checked
Exception(ajava.lang.Exceptionthat isn't aRuntimeException) → commit.
@Transactional
public void process(Order o) throws IOException {
repo.save(o);
sendFile(o); // throws IOException (checked)
// -> the save above is COMMITTED, because checked exceptions don't roll back
}
That silent commit on a checked exception is the trap. If you want the save undone, you must override the rule.
Overriding it
rollbackFor adds exception types that should roll back (typically a checked one):
@Transactional(rollbackFor = IOException.class)
public void process(Order o) throws IOException { ... } // now rolls back
noRollbackFor does the opposite — names exceptions that should not roll back even though they normally would (e.g. a RuntimeException you treat as a benign, expected signal):
@Transactional(noRollbackFor = OptimisticLockRetryHint.class)
public void apply() { ... } // this runtime exception commits anyway
Both accept arrays, and matching is by assignability (subclasses included). When multiple rules match, the most specific (closest in the type hierarchy) wins.