13 Using transactions in Spring apps
Accuracy in data-changing applications depends on controlling how multiple related operations behave together. The chapter explains transactions as a way to group several mutable operations into one logical unit of work so that they either all succeed or all fail. Using a money-transfer example, it shows why this matters: if money is withdrawn from one account but not deposited into another, the system becomes inconsistent. Transactions solve this by providing atomicity, meaning changes are committed only when every step succeeds, and rolled back when something goes wrong.
The chapter then explains how Spring supports this behavior through an AOP-based transaction mechanism. Spring provides the transaction aspect behind the scenes, and developers mark the method to be managed with the annotation used for transactional code. When the method runs successfully, Spring commits the transaction and persists the changes; if a runtime exception escapes the method, Spring rolls the transaction back and restores the previous state. A key detail is that the exception must be thrown out of the method for Spring to detect it. By default, checked exceptions do not trigger rollback, though Spring’s rollback rules can be customized if needed.
To demonstrate the idea in practice, the chapter builds a small Spring application backed by an in-memory database with repository, service, and controller layers. The repository reads and updates account records, while the service performs the transfer by loading both accounts, calculating new balances, and saving the changes. The service method is marked as transactional, and the controller exposes endpoints to transfer money and inspect all accounts. The example is tested twice: once with a successful transfer, which updates both balances, and once with a forced runtime exception, which proves that Spring rolls back the transaction so the original account values remain unchanged.
An example of a use case. When transferring money from one account to another account, the app executes two operations: it subtracts the transferred money from the first account and adds it to the second account. We’ll implement this use case, and we need to make sure its execution won’t generate inconsistencies in data.
If one of the steps of a use case fails, data becomes inconsistent. For the money transfer example, if the operation that subtracts the money from the first accounts succeeds, but the operation that adds it to the destination account fails, money is lost.
A transaction solves possible inconsistencies that could appear if any of the steps of a use case fail. With a transaction, if any of the steps fail, the data is reverted to how it was at the transaction start.
When you use the @Transactional annotation with a method, an aspect configured by Spring intercepts the method call and applies the transaction logic for that call. The app doesn’t persist the changes the method makes if the method throws a runtime exception.
If a runtime exception is thrown inside the method, but the method treats the exception and doesn’t throw it back to the caller, the aspect won’t get this exception and will commit the transaction. When you treat an exception in a transactional method, such as in this case, you need to be aware the transaction won’t be rolled back, as the aspect managing the transaction cannot see the exception.
We implement the transfer money use case in a service class and expose this service method through a REST endpoint. The service method uses a repository to access the data in the database and change it. The service method (which implements the business logic) must be wrapped in a transaction to avoid data inconsistencies if problems occur during the method execution.
The transaction starts just before the service method execution and ends just after the method successfully ends. If the method doesn’t throw any runtime exception, the app commits the transaction. If any step causes a runtime exception, the app restores the data to how it was before the transaction started.
When the method throws a runtime exception, Spring rolls back the transaction. All the successful changes made on the data are not persisted. The app restores the data to how it was when the transaction started.
Summary
- A transaction is a set of operations that change data, which either execute together or not at all. In a real-world scenario, almost any use case should be the subject of a transaction to avoid data inconsistencies.
- If any of the operations fail, the app restores the data to how it was at the beginning of the transaction. When that happens, we say that the transaction rolls back.
- If all the operations succeed, we say the transaction commits, which means the app persists all the changes the use case execution did.
- To implement transactional code in Spring, you use the @Transactional annotation. You use the @Transactional annotation to mark a method you expect Spring to wrap in a transaction. You can also annotate a class with @Transactional to tell Spring that any class methods need to be transactional.
- At execution, a Spring aspect intercepts the methods annotated with @Transactional. The aspect starts the transaction, and if an exception occurs the aspect rolls back the transaction. If the method doesn’t throw an exception, the transaction commits, and the app persists the method’s changes.
Spring Start Here, Second Edition ebook for free