Skip to content

Transactions 101: when and how to use them in the Spring Framework

If you’ve worked with Java software that uses the Spring Framework, you probably have come across the transactional annotation in the code base. If you’re like me, you probably didn’t fully understand how they work and why we would use them.
Nov 26, 2024 9:08:33 AM Raigardas Tautkus, Software Developer

In this article I will delve into the basics and help you understand the basics of transactions, explain how the @Transactional annotation works in the Spring Framework and highlight when to use and when not to use transactions.


What is a transaction?

A transaction can be seen as a set of actions that must all succeed together to actually go through. We can take bank transfers as an example: you need to remove the money from one account and add it to another. If one of the steps fails, you don’t want only one part of it to go through – you want the whole operation to succeed or fail as a unit. This is basically what is a transaction in this context.

In the Spring Framework, a transaction is a way to group database operations, such as insert, update, or delete, so that they are either all successfully completed and committed, or none of them. If any operation fails, all changes are rolled back.

Transactions usually use these four rules, known as ACID:

  • Atomicity: all operations in a transaction complete, or none do.
  • Consistency: data stays in a valid state before and after the transaction.
  • Isolation: transactions do not interfere with each other.
  • Durability: once completed, transactions are saved, even if the system crashes.

Why do we need transactions?

Without transactions, data in some operations (especially multi-step or cascading operations) can be inconsistent. For example, if only one part of the bank transfer completed successfully, the sender might be charged without the recipient being given the money. Transactions help prevent these kinds of problems. Along with that, transactions in Spring also come with some additional features, such as rollbacks, isolation levels, propagation, and others.

Stages of transactions

There are three main stages to transactions: start, commit, and rollback.

  1. Start: when a method that is a transcation begins executing, the transaction starts. Operations in the method are part of a single transaction and are not permanent until committed.
  2. Commit: if the method completes successfully without errors the transaction is committed. All changes made in the transaction are applied to the database permanently. After commit, the transaction is complete and the changes are applied to the database.
  3. Rollback: if an exception happens during the execution of the method, the transaction performs rollback. Rollback undoes the changes made during the transaction and leaves the database in the state as if the transaction never happened. Without rollbackFor attribute, Spring by default only does rollback on unchecked exceptions. Rollbacks can prevent partial and incorrect updates, in case one of the operations fails.

Transactions in Spring

The @Transactional annotation in Spring Framework simplifies working with transactions. Instead of manually writing code to start, commit or rollback transactions, @Transactional handles all of that, and more, for us.

How to use transactional in Java Spring methods

Using @Transactional is very simple in the Spring Framework, all you have to do is add the annotation to a method, most of the time it should be a service layer method. That essentially says Spring to make this method transactional. For specific configuration, you will need to pass some attributes to the annotation. I will dive deeper into that topic in another article.

Below is an example of using @Transactional in a simple mock example of using transactions for money transfering in the BankService:

transactions code

In this example, we have two methods which will be encapsulated within an transaction.

  • When the transferMoney method is called, @Transactional does all the work and transforms the operation into a transaction. The method is now a transaction in the start phase.
  • If both the removeFrom and the addTo methods go through successfuly without error, the transaction is committed. That is also known as commit stage.
  • If there is an error in either of the methods, the transaction is rolled back to prevent partial updates. In case of an error, the transaction goes into the rollback stage.

The principle works the same when we have an operation with cascading changes (can be update, insert, delete, etc.), by default, unless we configure it otherwise. All of the cascading changes will have to complete for it to commit, otherwise the whole transaction will be rolled back.

When it can be beneficial to use transactions in Java

Transactions are very useful for use cases that need to maintain data integrity, especially in complex business operations that need multiple steps or updates to the database at once. A few scenarios when it might make sense to use them:

  1. In multi step database operations: when an operation uses multiple steps that all need to succeed or fail together, it is a good place to use transactions. In a banking system, transferring money from one account to another requires both to withdraw money from the sender and to deposit money to the recipient. When you use transactions, it ensures that if one of the step fails, the database remains consistent and transcations are not commited.
  2. When the operation involes cascading updates: you should use transactions when an operation in one table may affect data in related tables. For example, deleting a user record might need cascading deletions in other tables, like skills table or career paths table, that are associated with the user. Using the @Transactional annotation here ensures that all related changes are successfully applied, or none are. That will help maintain data integrity.
  3. When you deal with data that is highly volatile: in cases where failures might happen, transactions allow rolling back to the last consistent state. This is helpful for applications where an error during execution could cause data corruption.
  4. When you are dealing with concurrency: when multiple users or systems access and modify the same data, transactions can help isolate actions, preventing them from interfering with each other and causing concurrency issues that both actions modified the same resources. With transactions, it ensures that each transaction’s changes are temporarily invisible to others until it’s complete.

When it might not be necessary to use transactions in Java

While transactions offer powerful tools for data consistency, they can add weight and complexity. Here are a few scenarios of when you might not need to use transactions:

  1. In single step, non-cascading updates: for simple updates that only modify a single table or record at a time, which do not have cascading related table updates, don’t require multiple steps, and don’t require consistency, transactions are likely not necessary.
  2. When you don’t need strict consistency: if the methods make database updates that don’t need a lot of consistency, transactions could slow it down.
  3. Methods that do not modify data: methods that only check, validate data, log events, and do not alter the database, usually don’t need transactions. Transactions here would slow down the method, usually for little benefit.

Key notes of the transactional annotation

  1. It is best to use @Transactional on service layer methods, where the business logic is, not on repository layer methods.
  2. The @Transactional annotation will not work on private methods because of Spring AOP Proxy Mechanism. There are ways to overcome that, I will explain more about this in another article.
  3. If you are using a Spring Data JPA Repository (or any repositories that extend CrudRepository), transaction management is automatically applied to all methods that modify the database (such as save , delete, etc). But note that you will still need to use the annotation to configure specific transaction behavior for isolation levels, propagation, rollback rules and to meet some requirements you may have.
  4. If you are using @Transactional and need to do read operations, you can add a readOnly flag to it @Transactional(readOnly = true). This will ensure that the transaction can only do read operations and allow the database to use efficient read-only isolation levels.
  5. @Transactional has propagation and isolation, which allow more control over transactions. It is possible to configure. Propagation defines how a transaction should behave in a nested transaction. Isolation define how transactions interact with each other and how they see data changes made by other transactions. These are beyond the scope of this article, but they are worth exploring for more advanced use cases.

Conlusion

The @Transactional annotation is used for transaction management in the Spring Framework to mark an operation as a transaction. It is very useful for multi-step operations, where ensuring data consistency is essential. By using @Transactional, you can avoid writing boilerplate code to manually manage transactions, leading to cleaner and more reliable code.

Use @Transactional for methods that perform multiple database operations that must either all succeed or fail together, especially when dealing with important or complex data. Avoid using it for simple updates, methods that don’t interact with the database, or operations involving cache data, as it can add additional unnecessary weight.

If you’re working with databases often in your Spring application, I highly recommend diving deeper into @Transactional for better control over your database operations and improved application reliability. Just be sure to use them correctly :)

Related posts