How to configure rollback for checked exceptions?
By default, Spring does not roll back transactions on checked exceptions (subclasses of Exception, but not RuntimeException). To fix this, use the rollbackFor parameter.
🟢 Junior Level
By default, Spring does not roll back transactions on checked exceptions (subclasses of Exception, but not RuntimeException). To fix this, use the rollbackFor parameter.
Simplest way
@Transactional(rollbackFor = Exception.class)
public void businessMethod() throws MyCheckedException {
// Now ANY exception will cause rollback
}
// rollbackFor tells TransactionInterceptor: “treat this exception as a // rollback signal too”. Check: if exception instanceof rollbackFor → setRollbackOnly().
Analogy
Think of a fire alarm: by default it only triggers on fire (RuntimeException). Checked exception is a gas leak, which also needs handling. rollbackFor tells the system: “any emergency = evacuate”.
For specific exception
@Transactional(rollbackFor = IOException.class)
public void readFile() throws IOException {
// IOException will cause rollback
}
For multiple exceptions
@Transactional(rollbackFor = {IOException.class, MyBusinessException.class})
public void process() throws IOException, MyBusinessException {
// Both exceptions will cause rollback
}
Golden rule
If your business exception means the operation cannot complete successfully — it should trigger rollback. Use rollbackFor = Exception.class for safety.
When NOT to change rollback rules
- If exception = bug signal (NPE, IAE) — default rollback is correct
- If no clear team policy — different rollbackFor in different services creates chaos
- If using @ControllerAdvice — let exception reach controller, then decide what to return to client
🟡 Middle Level
Why this matters
In real projects, services often use custom business exceptions. If they inherit from Exception (checked), Spring will COMMIT by default when they occur. This can leave the database in an inconsistent state.
// BAD — InsufficientFundsException extends Exception
@Transactional
public void withdraw(BigDecimal amount) throws InsufficientFundsException {
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
// Spring will COMMIT despite the exception!
}
Three approaches to solve it
1. rollbackFor (recommended for checked)
@Transactional(rollbackFor = Exception.class)
public void withdraw(BigDecimal amount) throws InsufficientFundsException {
// Rollback for all exceptions
}
2. Switch to RuntimeException
// InsufficientFundsException now extends RuntimeException
@Transactional
public void withdraw(BigDecimal amount) {
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException(); // Rollback automatically
}
}
3. Programmatic rollback
@Transactional
public void withdraw(BigDecimal amount) {
try {
if (balance.compareTo(amount) < 0) {
throw new InsufficientFundsException();
}
} catch (InsufficientFundsException e) {
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
throw e;
}
}
Common mistakes
| Mistake | What happens | How to fix |
|---|---|---|
Forgetting rollbackFor |
COMMIT on checked exception | rollbackFor = Exception.class |
Typo in rollbackForClassName |
Rule silently doesn’t fire | Use rollbackFor with Class literal |
Conflicting rollbackFor + noRollbackFor |
Unpredictable behavior | Unambiguous rules |
Catch without setRollbackOnly() |
Exception caught → COMMIT | Call setRollbackOnly() in catch |
Approach comparison
| Approach | Pros | Cons | When to use |
|---|---|---|---|
rollbackFor = Exception.class |
Simple, reliable | Rollback even for “non-errors” | Standard approach |
rollbackFor = {SpecificException.class} |
Precise control | Need to list all | When some checked exceptions = not error |
| Switch to RuntimeException | Automatic rollback, less boilerplate | Requires exception hierarchy refactor | New project or refactoring |
Programmatic setRollbackOnly() |
Conditional rollback | Boilerplate, Spring coupling | Complex business logic |
When NOT to use rollback for checked exceptions
| Situation | Why | Alternative |
|---|---|---|
| Business validation | Expected situation, data valid | noRollbackFor = ValidationException.class |
| Graceful degradation | System should keep working | Not exception, but result object |
| Partial success | Part of data is correct | Handle, commit, log error |
🔴 Senior Level
rollbackFor Attribute Resolution
Spring resolves rollback rules through priority chain:
// AbstractFallbackTransactionAttributeSource
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) {
for (TransactionAnnotationParser parser : annotationParsers) {
TransactionAttribute attr = parser.parseTransactionAnnotation(ae);
if (attr != null) return attr;
}
return null;
}
// SpringTransactionAnnotationParser
public TransactionAttribute parseTransactionAnnotation(Transactional ann) {
RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
Class<? extends Throwable>[] rollbackFor = ann.rollbackFor();
for (Class<? extends Throwable> ex : rollbackFor) {
rbta.getRollbackRules().add(new RollbackRuleAttribute(ex));
}
String[] rollbackForClassName = ann.rollbackForClassName();
for (String ex : rollbackForClassName) {
rbta.getRollbackRules().add(new RollbackRuleAttribute(ex));
}
Class<? extends Throwable>[] noRollbackFor = ann.noRollbackFor();
for (Class<? extends Throwable> ex : noRollbackFor) {
rbta.getRollbackRules().add(new NoRollbackRuleAttribute(ex));
}
return rbta;
}
String-Based Rollback Rules — Hidden Danger
@Transactional(rollbackForClassName = {"MyBusinessException"})
public void process() { ... }
Spring resolves class name at runtime:
public RollbackRuleAttribute(String exceptionPattern) {
this.exceptionName = exceptionPattern;
// NOT validated at startup — resolved lazily during exception handling
}
Risk: Typo → rule silently doesn’t fire → no rollback. Use rollbackFor with Class literal for type safety.
Combining rollbackFor and noRollbackFor
@Transactional(
rollbackFor = Exception.class,
noRollbackFor = {NotFoundException.class, ValidationException.class}
)
public void process() {
// Exception → rollback
// NotFoundException → NO rollback (commit despite exception)
// ValidationException → NO rollback
}
Rule matching order:
- Most specific match wins (deepest in hierarchy)
- At same depth — first matching rule
- Default rules (RuntimeException, Error) checked last
// noRollbackFor = “this is a RuntimeException, but DON’T rollback”. Useful for // expected business exceptions that inherit from RuntimeException.
Programmatic Rollback — When and Why
Scenario 1: Checked exception without propagation
@Transactional
public void processFiles(List<File> files) {
for (File file : files) {
try {
processFile(file); // throws IOException
} catch (IOException e) {
log.error("File {} failed", file.getName());
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
break; // Stop processing, but mark for rollback
}
}
}
Scenario 2: Conditional rollback
@Transactional
public void transferWithLimit(Long from, Long to, BigDecimal amount) {
if (amount.compareTo(DAILY_LIMIT) > 0) {
log.warn("Exceeds daily limit");
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
return; // Method returns normally, but transaction rolls back
}
// ... transfer logic
}
Edge Cases (minimum 3)
-
String typo in
rollbackForClassName:@Transactional(rollbackForClassName = "BusinesException")— silently ignored. Spring doesn’t validate class name at initialization. -
Conflicting rules:
rollbackFor = Exception.class+noRollbackFor = RuntimeException.class. Specificity determines winner. RuntimeException is deeper →noRollbackForwins → no rollback for RuntimeException, rollback for checked. -
AOP ordering: Custom exception handler aspect may catch exception before
TransactionInterceptor. Interceptor sees normal return → COMMIT. Solution:@Orderto control ordering. -
Self-invocation bypass: Calling
this.method()inside class — proxy not involved, rollback rules not applied at all. No transaction = no rollback. -
Async methods:
@Asyncwith checked exceptions — exception doesn’t propagate to caller’s transaction context. Caller continues with COMMIT. Future contains exception — needfuture.get().
Global Rollback Configuration
@Configuration
public class TransactionConfig {
@Bean
public TransactionInterceptor transactionInterceptor(
PlatformTransactionManager tm) {
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionManager(tm);
RuleBasedTransactionAttribute defaultRule =
new RuleBasedTransactionAttribute();
defaultRule.setRollbackRules(
List.of(new RollbackRuleAttribute(Exception.class)));
NameMatchTransactionAttributeSource source =
new NameMatchTransactionAttributeSource();
source.addTransactionalMethod("*", defaultRule);
interceptor.setTransactionAttributeSource(source);
return interceptor;
}
}
Migration Strategy: From Checked to Unchecked
// Step 1: Add rollbackFor to existing methods
@Transactional(rollbackFor = Exception.class)
public void oldMethod() throws BusinessException { ... }
// Step 2: Change exception hierarchy
public class BusinessException extends RuntimeException { ... } // Was extends Exception
// Step 3: Remove throws and rollbackFor
@Transactional
public void newMethod() { ... } // Clean
// Step 4: Update callers (no longer need try-catch)
service.newMethod(); // Instead of: try { service.oldMethod(); } catch...
Performance Numbers
| Operation | Time |
|---|---|
| Rollback rule resolution (cached) | ~0.5 μs |
rollbackFor class matching |
~0.1 μs |
| String-based resolution | ~5-10 μs (class loading) |
| Actual DB rollback | 1-5 ms (PostgreSQL), 5-50 ms (InnoDB) |
Memory Implications
RollbackRuleAttributeobjects — ~100 bytes each, stored inTransactionAttribute- String-based rules: Class loading at runtime — memory overhead if exception class is large
- Exception stack traces — can be large. With frequent rollbacks = GC pressure.
Thread Safety
Rollback rules are immutable — thread-safe. ThreadLocal state in TransactionSynchronizationManager — concurrent calls are isolated.
Production War Story
Financial service: withdraw() threw InsufficientFundsException extends Exception. @Transactional without rollbackFor. On insufficient funds exception — transaction COMMITed, balance debited, but transfer didn’t complete. Money disappeared. Fix: rollbackFor = Exception.class + migration to RuntimeException hierarchy.
Monitoring
Debug logging:
logging:
level:
org.springframework.transaction.interceptor: TRACE
Micrometer:
@Bean
public TransactionSynchronization transactionMetrics(MeterRegistry registry) {
return new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
registry.counter("transaction.rollback").increment();
}
}
};
}
Highload Best Practices
- Baseline:
rollbackFor = Exception.classon all write methods. Without exceptions. - Minimize checked exceptions: Convert business exceptions to RuntimeException. Aligns with Spring philosophy.
- Global configuration: Use
TransactionInterceptorwith global rollback rules instead of duplicating annotations. - Never use
rollbackForClassName: String-based rules — silent failure risk. Use Class literals. - Test rollback: Integration tests should verify rollback actually happens for each exception type.
- Monitor rollback rate: High % rollback = problem in validation/business logic.
🎯 Interview Cheat Sheet
Must know:
- rollbackFor = Exception.class — rollback for all exceptions, recommended baseline
- rollbackFor = {SpecificException.class} — precise control for specific types
- Programmatic rollback: TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
- rollbackForClassName (string) — risky: typo silently ignored, use Class literals
- noRollbackFor excludes specific exceptions from rollback (e.g., NotFoundException)
- Rule matching order: most specific match (deepest in hierarchy) wins
Common follow-up questions:
- Three approaches to checked exception rollback? — rollbackFor, switch to RuntimeException, programmatic setRollbackOnly
- Why is rollbackForClassName dangerous? — String resolution lazy, typo not validated at startup
- When to use programmatic rollback? — Conditional rollback, complex business rules
- Global rollback configuration — how? — TransactionInterceptor with RuleBasedTransactionAttribute
Red flags (DO NOT say):
- “rollbackForClassName = safe way” — silent failure on typo
- “Catch exception = automatic rollback” — Spring doesn’t see caught exceptions
- “Can mix rollbackFor and noRollbackFor carelessly” — conflicting rules create unpredictable behavior
Related topics:
- [[19. Which exceptions cause rollback by default]]
- [[18. What is rollback in transactions]]
- [[16. What is @Transactional annotation]]