CodeGym /Courses /Module 5. Spring /Exceptions in transactions and how to handle them

Exceptions in transactions and how to handle them

Module 5. Spring
Level 6 , Lesson 8
Available

Today our goal is to get a solid grip on exceptions in the context of transactions. Why does it matter? Because failures happen, and you need to know how to handle them so the data in your database doesn't turn into a mess.


Exceptions in the world of transactions

As programmers, we live in a world of exceptions. They lurk around every corner: from NullPointerException when calling a method on an innocent null to SQLException on SQL errors. When it comes to transactions, exceptions get even trickier, because an error can mean that the data in the database ends up in an inconsistent state.

Main types of exceptions in transactions

In the context of transactions, exceptions fall into two big camps:

  1. System exceptions. These are the surprises we can't always predict: for example, a lost DB connection (JDBCException), network errors, or running out of memory. They typically mean the transaction should be rolled back.
  2. Application exceptions. These are the errors we can and should handle: business-logic violations, like exceeding a bank account limit or trying to register a user with an email that already exists. Such exceptions may or may not require a rollback — it depends on the context.

How do transactions deal with exceptions in Spring?

In Spring, transaction management automation is handled by the @Transactional annotation. When an error happens inside a method annotated with @Transactional, Spring takes control: it either rolls back the transaction or leaves the changes in the DB, depending on the rule.

Transaction rollback

By default, Spring automatically rolls back a transaction if an unhandled exception of type RuntimeException or Error occurs. That means if your method throws, for example, NullPointerException or IllegalArgumentException, all changes made to the DB will be rolled back.

However, checked exceptions (for example, SQLException) do not trigger a rollback by default. That's because Spring follows the idea that checked exceptions should be handled explicitly in code.


Examples of exception handling

Let's start with a basic example. Suppose we have a service that works with bank accounts. We want to debit money from one account and credit another. If an error occurs (for example, not enough funds in the source account), the transaction should be rolled back.


@Service
public class BankTransactionService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        Account fromAccount = accountRepository.findById(fromAccountId)
                .orElseThrow(() -> new RuntimeException("Sender account not found"));

        Account toAccount = accountRepository.findById(toAccountId)
                .orElseThrow(() -> new RuntimeException("Recipient account not found"));

        if (fromAccount.getBalance().compareTo(amount) < 0) {
            throw new RuntimeException("Not enough funds in sender's account");
        }

        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}

In this example, if the sender's balance is less than the requested amount, the method will throw a RuntimeException, and all database changes will be rolled back automatically.

Controlling rollback with rollbackFor

If you want checked exceptions (like SQLException) to also cause a rollback, you need to explicitly state that using the rollbackFor parameter. Here's an example:


@Transactional(rollbackFor = {SQLException.class})
public void performDatabaseOperation() throws SQLException {
    // Database operations
    throw new SQLException("Database error");
}

Now, if a SQLException occurs, the transaction will be rolled back.

Exceptions that shouldn't trigger rollback

Sometimes you want certain exceptions not to roll back the transaction, even if they're RuntimeException. For that you use the noRollbackFor parameter:


@Transactional(noRollbackFor = {IllegalArgumentException.class})
public void processTransaction() {
    // Data processing
    throw new IllegalArgumentException("This exception won't roll back the transaction");
}

How to guarantee a transaction rollback?

Rollback is a data-protection mechanism. If a method throws an exception, Spring guarantees a rollback, but there are caveats:

  1. Don't wrap transactional methods in try-catch if you want a rollback. If you catch the exception and don't rethrow it, Spring assumes the method completed successfully and won't roll back the transaction.
    
    @Transactional
    public void updateAccount() {
        try {
            // Database actions
        } catch (Exception e) {
            // Log the error, but the exception isn't rethrown
        }
    }
    

    Here the transaction won't be rolled back, even if an error happened.


  2. Remember internal calls. If a transactional method calls another transactional method from inside the same class, Spring might not see the exception. Example of a wrong call:
    
    @Service
    public class MyService {
    
        @Transactional
        public void methodA() {
            methodB(); // The call happens inside the same class
        }
    
        @Transactional
        public void methodB() {
            // Logic
            throw new RuntimeException("Error!");
        }
    }
    

    Here the transaction won't roll back because methodB() was called directly, not through Spring's proxy object.


Exception handling and logging

Handling exceptions is not just useful, it's necessary. At a minimum you should log them, otherwise during debugging you'll spend more time than actual development. Here's an example:


@Autowired
private Logger logger;

@Transactional
public void deleteAccount(Long accountId) {
    try {
        accountRepository.deleteById(accountId);
    } catch (DataAccessException e) {
        logger.error("Failed to delete record with ID {}", accountId, e);
        throw e; // or rethrow the exception
    }
}

Common mistakes when working with exceptions in transactions

1. Catching and ignoring exceptions

If you handle an exception and don't signal an error (don't rethrow it), the transaction won't be rolled back. This can lead to inconsistent data state.

2. Using transactions for reads

If you set @Transactional(readOnly = true) and still make changes to the DB, this can lead to unexpected behavior. For example, such changes might be ignored.


Practical application

Transactions and their handling are critical in enterprise software development. Imagine an online store: a user adds an item to the cart, checks out — and at that moment the transaction must guarantee that the item is reserved, and the stock quantity is reduced. If something goes wrong — no partial operations! Only rollback.

On interviews, questions about exception handling and configuring @Transactional come up all the time. Your knowledge will help you explain how to avoid mistakes and show that you understand the system's nuances.

Don't forget, the golden rule of transactions: all or nothing!

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION