Question 20 · Section 11

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.

Language versions: English Russian Ukrainian

🟢 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

  1. If exception = bug signal (NPE, IAE) — default rollback is correct
  2. If no clear team policy — different rollbackFor in different services creates chaos
  3. 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

@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:

  1. Most specific match wins (deepest in hierarchy)
  2. At same depth — first matching rule
  3. 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)

  1. String typo in rollbackForClassName: @Transactional(rollbackForClassName = "BusinesException") — silently ignored. Spring doesn’t validate class name at initialization.

  2. Conflicting rules: rollbackFor = Exception.class + noRollbackFor = RuntimeException.class. Specificity determines winner. RuntimeException is deeper → noRollbackFor wins → no rollback for RuntimeException, rollback for checked.

  3. AOP ordering: Custom exception handler aspect may catch exception before TransactionInterceptor. Interceptor sees normal return → COMMIT. Solution: @Order to control ordering.

  4. Self-invocation bypass: Calling this.method() inside class — proxy not involved, rollback rules not applied at all. No transaction = no rollback.

  5. Async methods: @Async with checked exceptions — exception doesn’t propagate to caller’s transaction context. Caller continues with COMMIT. Future contains exception — need future.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

  • RollbackRuleAttribute objects — ~100 bytes each, stored in TransactionAttribute
  • 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

  1. Baseline: rollbackFor = Exception.class on all write methods. Without exceptions.
  2. Minimize checked exceptions: Convert business exceptions to RuntimeException. Aligns with Spring philosophy.
  3. Global configuration: Use TransactionInterceptor with global rollback rules instead of duplicating annotations.
  4. Never use rollbackForClassName: String-based rules — silent failure risk. Use Class literals.
  5. Test rollback: Integration tests should verify rollback actually happens for each exception type.
  6. 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]]