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.
- 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.
- 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.
- 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:
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 theaddTo
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:
- 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.
- 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. - 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.
- 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:
- 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.
- 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.
- 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
- It is best to use
@Transactional
on service layer methods, where the business logic is, not on repository layer methods. - 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. - 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. - 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. @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 :)