This is an issue when a method is annotated with @Transactional and declares one or more checked exceptions in its throws
clause, but does not explicitly configure how the transaction should behave when those exceptions occur.
In Spring Framework, the @Transactional annotation controls database transactions. By default:
RuntimeException or Error) automatically trigger a transaction
rollbackWhen you declare a checked exception in your method signature but don’t specify the rollback behavior, Spring will commit the transaction even if that exception occurs. This often leads to data being saved in an inconsistent state.
When a method throws a checked exception, it typically signals that something went wrong during execution. In the context of database transactions, you usually want to roll back the transaction when errors occur to maintain data integrity.
However, Spring’s @Transactional annotation only rolls back transactions automatically for unchecked exceptions (like
RuntimeException). For checked exceptions, the transaction commits by default - even when the exception indicates a failure.
This default behavior creates a dangerous mismatch between intent and outcome:
This silent failure mode is particularly problematic because:
By explicitly specifying rollbackFor or noRollbackFor, you make the intended behavior clear and prevent unexpected
transaction commits.
When checked exceptions don’t trigger rollbacks as expected, the consequences can be severe:
Partial transactions commit to the database, leaving data in an inconsistent state. For example, if an order processing method saves an order but fails when creating the invoice, you end up with an order record but no corresponding invoice.
The application throws an exception indicating failure, but the database changes persist anyway. Error handling code may log the error or show a failure message to users, while the "failed" operation actually modified the database.
Multi-step operations that should be atomic (all-or-nothing) can partially complete. This violates business rules that depend on data consistency, such as:
Because the transaction commits silently, these bugs are hard to diagnose. The code appears to handle errors correctly (it throws and catches exceptions), but the database state doesn’t match expectations.
Explicitly specify rollbackFor to include the checked exceptions that should trigger a rollback. This is the most common fix, as most
checked exceptions represent error conditions that should prevent the transaction from committing.
Choose your approach based on the exception’s meaning:
rollbackFor with specific exception types when you know exactly which exceptions represent failures requiring rollbackrollbackFor = Exception.class when the method throws Exception or multiple checked exception types that all
represent failuresnoRollbackFor only when a checked exception represents an expected, recoverable condition where you intentionally want the
transaction to commit (such as a notification failure after successfully saving core data)
@Transactional
public void processOrder(Order order) throws IOException, SQLException { // Noncompliant
orderRepository.save(order);
notificationService.sendConfirmation(order); // May throw IOException
}
@Transactional(rollbackFor = {IOException.class, SQLException.class})
public void processOrder(Order order) throws IOException, SQLException {
orderRepository.save(order);
notificationService.sendConfirmation(order); // May throw IOException
}
If you want to roll back on all exceptions (both checked and unchecked), you can specify Exception.class as the rollback trigger. This
is a safe default when you’re unsure which specific exceptions to list.
@Transactional
public void importData(File file) throws Exception { // Noncompliant
List<Record> records = parser.parse(file);
recordRepository.saveAll(records);
}
@Transactional(rollbackFor = Exception.class)
public void importData(File file) throws Exception {
List<Record> records = parser.parse(file);
recordRepository.saveAll(records);
}
In rare cases where you intentionally want the transaction to commit even when a checked exception is thrown, use noRollbackFor to
document this decision explicitly. This makes it clear the behavior is intentional, not an oversight.
@Transactional
public void processWithNotification(Order order) throws NotificationException { // Noncompliant
orderRepository.save(order);
// We want order saved even if notification fails
notificationService.sendEmail(order);
}
@Transactional(noRollbackFor = NotificationException.class)
public void processWithNotification(Order order) throws NotificationException {
orderRepository.save(order);
// We want order saved even if notification fails
notificationService.sendEmail(order);
}