Transactions in Spring Boot
- General
Transactions in Spring Boot
Transactions are essential in any application that interacts with a database, as they ensure that changes to the database are atomic, consistent, isolated, and durable (ACID).
In this blog, we will explore how to use Spring Boot transactions to ensure data integrity in your applications.
What are Transactions?
A transaction is a sequence of operations that are treated as a single unit of work. In the context of a database, a transaction typically involves reading or modifying data in the database. Transactions are important because they ensure that changes to the database are atomic, consistent, isolated, and durable. Let’s break down these concepts:
- Atomicity: A transaction is atomic if it either completes all of its operations or rolls back to its initial state if any operation fails. In other words, if any part of a transaction fails, the entire transaction is rolled back.
- Consistency: A transaction is consistent if it takes the database from one valid state to another. For example, if a transaction transfers money from one account to another, the total amount of money in the system should remain the same.
- Isolation: A transaction is isolated if its execution appears to be the only one happening in the system. This ensures that multiple transactions running concurrently do not interfere with each other.
- Durability: A transaction is durable if its effects are permanent, even in the event of a system failure. Once a transaction has been committed, its changes should be durable.
Using Spring Boot Transactions
Spring Boot provides a simple and elegant way to work with transactions using @Transactional
annotation. This annotation can be applied to methods, and it ensures that the method is executed within a transactional context. If the method completes successfully, the transaction is committed. If the method throws an exception, the transaction is rolled back.
There are several attributes that can be used with the @Transactional
annotation to customize its behavior. Here are some of the most commonly used attributes:
propagation
: Defines how the transaction should propagate to other methods. For example, if a transaction is already in progress, should the method join the existing transaction, or should it create a new transaction? The default value isPropagation.REQUIRED
, which means that the method should join the existing transaction if one exists, or create a new transaction if one does not exist.isolation
: Defines the isolation level of the transaction. The isolation level determines how changes made by one transaction are visible to other transactions. The default value isIsolation.DEFAULT
, which means that the database’s default isolation level is used.readOnly
: Indicates whether the transaction is read-only or read-write. If the method only reads data from the database, it’s a good practice to set this attribute totrue
to improve performance. The default value isfalse
.timeout
: Defines the timeout for the transaction, in seconds. If the transaction takes longer than the specified timeout, it will be rolled back. The default value is-1
, which means that there is no timeout.rollbackFor
: Defines the exception types that should trigger a transaction rollback. By default, a transaction is rolled back if any runtime exception is thrown. You can use this attribute to specify which exceptions should trigger a rollback.
Let’s take a look at an example of how to use the @Transactional
annotation with some of these attributes:
1 2 3 4 5 6 7 8 9 10 11 |
public class ExampleService { @Autowired private ExampleServiceHelper exampleServiceHelper; @Transactional public void function1() { exampleServiceHelper.function2(); } } |
1 2 3 4 5 6 7 |
public class ExampleServiceHelper { @Transactional public void function2() { //some code } } |
In the above example, we can see that logically no. of Transactions created are: 2 (as both the calling and callee functions have @Transactional annotation on them)
But, actually, no. of Transaction created is just one. This is because the default propagation of transactions is Propagation.REQUIRED
, which means the transaction in function2() joins the existing transactional context(i.e of function1() ) instead of creating a new one.
If setting the propagation attribute to Propagation.REQUIRES_NEW, function2() will create a separate transaction, independent of any other transactional contexts that may be active.
Note : By default, if an exception is thrown within the method annotated with Propagation.REQUIRES_NEW, the transactional context for that method will be rolled back, and any changes made within that context will be undone. However, any changes made within the outer transactional context will remain committed.
Modifying the function2() as :
1 2 3 4 5 6 7 8 |
public class ExampleServiceHelper { @Transactional(propagation = Propagation.REQUIRES_NEW) public void function2() { // some code throw new RuntimeException("Something went wrong!"); } } |
Thus, in the above example, if an exception is thrown within function2()
, the transactional context for that method will be rolled back, and any changes made within that context will be undone. However, the outer transactional context created by function1()
will remain committed, and any changes made within that context will be persisted.
Note : By default, checked exceptions do not trigger a rollback of the transactional context, only unchecked exceptions do. This means that if a checked exception is thrown by a method within a transactional context, the transaction will not be rolled back by default. However, you can configure which exceptions trigger a rollback by specifying the
rollbackFor
ornoRollbackFor
attributes of the@Transactional
annotation.
For example, to specify that all exceptions trigger a rollback, you can use the rollbackFor
attribute with the Exception
class:
1 2 3 4 5 6 7 |
public class ExampleServiceHelper { @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void function2() { // some code throw new RuntimeException("Something went wrong!"); } } |
In this example, any exception is thrown within the function2() method, whether checked or unchecked, will trigger a rollback of the transactional context.
Alternatively, you can use the noRollbackFor
attribute to specify exceptions that should not trigger a rollback:
1 2 3 4 5 6 7 |
public class ExampleServiceHelper { @Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = IllegalArgumentException.class) public void function2() { // some code throw new RuntimeException("Something went wrong!"); } } |
In this example, if an IllegalArgumentException
is thrown within the function2() method, the transactional context will not be rolled back.
It’s important to carefully consider which exceptions should trigger a rollback in a transactional context to ensure that data consistency and integrity are maintained while avoiding unintended data loss or corruption.
Use cases of @Transactional in transactions :
- Transfer of funds between accounts: When transferring funds between two accounts, it is important to ensure that the transaction is atomic and consistent. The
@Transactional
annotation can be used to wrap the transfer operation in a transaction, so that either the entire transfer succeeds or fails. - Order processing: When processing an order, it may involve updating multiple tables in the database. The
@Transactional
annotation can be used to ensure that all the updates are done within a single transaction, so that the order is either processed completely or rolled back if any errors occur. - User registration: When a new user is registered in a system, it may involve creating records in multiple tables. The
@Transactional
annotation can be used to ensure that all the records are created within a single transaction, so that the user is either registered completely or rolled back if any errors occur. - Batch processing: When processing a large number of records, it is important to ensure that the processing is atomic and consistent. The
@Transactional
annotation can be used to wrap the batch processing operation in a transaction, so that either the entire batch is processed successfully or rolled back if any errors occur.
Conclusion :
In conclusion, the @Transactional
annotation is a powerful tool for managing database transactions in Spring Boot applications. It can be used to ensure that database operations are atomic, consistent, and isolated, which helps to maintain the integrity of the data and prevent inconsistencies that can arise from concurrent access to the database.
By understanding the different propagation levels and rollback rules, developers can use the @Transactional
annotation to control the behavior of transactions in their applications, and ensure that data is managed correctly in a variety of scenarios.
Related content
Auriga: Leveling Up for Enterprise Growth!
Auriga’s journey began in 2010 crafting products for India’s