Mastering Spring & MSA Transactions – Part 9: Which Exceptions Should Roll Back Your Transactions?: Fine-Tuning Spring’s Rollback Rules

In the previous articles, we explored how to start or join transactions (propagation) and how to handle concurrency (isolation). However, even if you have those aspects covered, you still need to decide what happens when exceptions occur:

  • By default, Spring rolls back the transaction on runtime exceptions (e.g., RuntimeException, Error),
  • but doesn’t roll back for checked exceptions (e.g., IOException, custom checked types).

What if you need to roll back on a checked exception like IOException? Or what if you want to avoid rolling back on a particular runtime exception? That’s where rollbackFor and noRollbackFor come in. Let’s dive in.

1) Spring’s Basic Rollback Policy

1.1) Runtime Exceptions = Rollback by Default

@Service
@Transactional
public class SomeService {
    
    public void doSomething() {
        throw new RuntimeException("Oops!");
        // By default, the transaction is rolled back
        // whenever a runtime exception occurs.
    }
}
  • Runtime exceptions (including RuntimeException and Error) trigger an automatic rollback by default.
  • Once RuntimeException is thrown, Spring rolls back and propagates the exception upward.

1.2) Checked Exceptions = No Rollback by Default

@Transactional
public void processFile(String filePath) throws IOException {
// If an IOException occurs,
// the transaction won't roll back by default.
readFileAndInsertDB(filePath);
}
  • By default, checked exceptions—like IOException, SQLException, or a custom checked type—do not lead to a rollback.
  • This can surprise developers who assume “any exception” would undo changes in the database. Without special settings, your DB commits despite the I/O failure.

2) rollbackFor: Forcing Rollback on Specific Exceptions

2.1) Rolling Back on Checked Exceptions

@Transactional(rollbackFor = {IOException.class, MyCheckedException.class})
public void importData(String filePath) throws IOException, MyCheckedException {
// If an IOException or MyCheckedException occurs,
// rollback is forced.
parseAndPersist(filePath);
}
  • By listing one or more exceptions in rollbackFor, you override Spring’s default for those exceptions—even if they’re checked.
  • You can specify multiple exceptions in the array. If your exception inheritance is broad (like Exception.class), it can encompass everything underneath it—so be careful not to cast the net too wide unless you truly want to roll back on all exceptions.

3) noRollbackFor: Excluding Certain Exceptions from Rollback

3.1) Overriding the Default for Runtime Exceptions

@Transactional(noRollbackFor = {ValidationException.class})
public void updateData(DTO dto) {
// Even if ValidationException extends RuntimeException,
// the transaction won't roll back;
// changes are committed.
validate(dto);
save(dto);
}
  • Typically, runtime exceptions cause rollback. By naming them in noRollbackFor, you ensure those exceptions won’t trigger a rollback.
  • Why might you do this? Maybe you want to record partial changes in the database, then throw a ValidationException to let the caller handle an error scenario—without undoing DB updates.

4) Real-World Examples

4.1) “File I/O Failure Must Roll Back Everything”

@Transactional(rollbackFor = IOException.class)
public void uploadAndInsert(String path) throws IOException {
// If file read fails,
// we definitely don't want partial DB changes.
// rollbackFor ensures transaction rollback on IOException.
readFile(path);
insertIntoDB(path);
}
  • Without rollbackFor, an IOException would be ignored by the default policy, leaving the DB changes committed even though the file operation failed.

4.2) “Runtime Exceptions That Should Not Roll Back”

@Transactional(noRollbackFor = BusinessRuleException.class)
public void payOrder(Order order) {
    // Typically, a runtime exception leads to rollback,
    // but BusinessRuleException is excluded here.
    // DB changes remain committed,
    // although the method throws an exception.
    recordPayment(order);
    if (!checkBusinessRules(order)) {
        throw new BusinessRuleException("Payment is invalid, but we keep the record!");
    }
}
  • This scenario might log or track a failed attempt but keeps the data. Possibly you want an audit trail of all attempts, even if some are marked invalid.

4.3) Mixed Usage

@Transactional(
rollbackFor = {IOException.class, ParseException.class},
noRollbackFor = {ValidationException.class}
)
public void complexOperation() {
// IOException, ParseException -> rollback
// ValidationException -> transaction commits
}

5) FAQs and Common Pitfalls

  1. Priority or Conflicts
    • By default: Runtime = rollback, Checked = commit.
    • If you list something in rollbackFor, it’s forced to roll back.
    • If you list something in noRollbackFor, it’s forced to commit.
    • Don’t put the same exception in both arrays or you’ll create contradictory instructions.
  2. Exception Inheritance
    • If you specify IOException.class, it covers FileNotFoundException too.
    • If you specify Exception.class, effectively every exception is included. That might be too broad.
  3. No Transaction, No Rollback
    • If a method isn’t actually within a transaction (for example, no @Transactional or a scenario where propagation is NOT_SUPPORTED), these rules do nothing because there’s no transaction to roll back or commit.
  4. Team Conventions
    • Many teams adopt a standard approach, e.g., “All I/O exceptions result in rollback,” or “Validation checks do not cause rollback.” Documenting such rules helps avoid confusion.

Conclusion

Designing which exceptions trigger rollbacks is as crucial as deciding when transactions begin and how they isolate data. By default, Spring’s runtime-only rollback can be too narrow, or too broad, depending on your domain.

  • rollbackFor: Force rollback on specific exceptions—great for dealing with critical checked exceptions that must revert DB changes if they occur.
  • noRollbackFor: Exclude certain runtime exceptions from rollback—handy when you want partial data changes preserved despite an error.

Ultimately, propagation tells us “how to start or join a transaction,” isolation handles “what data you see in that transaction,” and now rollback rules decide “which exceptions undo the transaction.” Putting these three angles together, you gain full control over your data’s consistency and error handling. And that’s exactly what robust enterprise applications need.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top