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:

When 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.

Why is this an issue?

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.

What is the potential impact?

When checked exceptions don’t trigger rollbacks as expected, the consequences can be severe:

Data Corruption

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.

Silent Failures

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.

Business Logic Violations

Multi-step operations that should be atomic (all-or-nothing) can partially complete. This violates business rules that depend on data consistency, such as:

Difficult Debugging

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.

How to fix it in Spring

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:

Code examples

Noncompliant code example

@Transactional
public void processOrder(Order order) throws IOException, SQLException { // Noncompliant
    orderRepository.save(order);
    notificationService.sendConfirmation(order); // May throw IOException
}

Compliant solution

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

Noncompliant code example

@Transactional
public void importData(File file) throws Exception { // Noncompliant
    List<Record> records = parser.parse(file);
    recordRepository.saveAll(records);
}

Compliant solution

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

Noncompliant code example

@Transactional
public void processWithNotification(Order order) throws NotificationException { // Noncompliant
    orderRepository.save(order);
    // We want order saved even if notification fails
    notificationService.sendEmail(order);
}

Compliant solution

@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);
}

Resources

Documentation