Transaction Control in Spring Boot
- General
Transaction Control in Spring Boot
What is a Transaction?
In the real world, we are surrounded by transactions every day. Financial ones, exchange, all interactions between 2 or more independent processes to produce the desired output could be considered as a transaction. Let’s explain it with a real-life example:
You have your mobile banking app and need to transfer some money to your friend. So, to do it, first, you open the application, authenticate your user, put your friend’s account number and transfer the money. Sounds simple, but what happened there?
- User authentication
- Your friend’s account validation
- Validation of you have enough money to transfer
- Discount money from your account
- Add money for the destiny account
In 5 steps, we were able to transfer money. Also, each step is completely independent of the other, for example, the account validation is a completely different process from the discount money from your account, so, as explained before, we have the interaction of more than 1 independent process to produce the desired output, in this case, money in your friend’s account. This is the base transactional concept, but there is one more thing to keep in mind: What should happen if one of the processes fails? Should continue? Should discard the operation? For being considered as a transaction, all processes inside should be executed successfully. If one process fails, all transactions is failed. This is the transactional principle.
A transaction must be committed only after the successful execution of the entire actions. Otherwise, the transaction should be rolled off in case of any exceptions. Below image depicts the same.
The @Transactional Annotation
With transactions configured, we can now annotate a bean with @Transactional either at the class or method level:
1 2 3 4 5 |
@Service @Transactional public class Example { //... } |
This service is tagged with @Transactional
, meaning that any failure causes the entire operation to roll back to its previous state and to re-throw the original exception. This means that none of the values are added to database if there is any failure at the runtime.
Isolation
Spring allows managing the isolation level. The default strategy for most databases is READ_COMMITTED
, but we have other ones such as: READ_UNCOMMITTED
, REPEATABLE_READ
and SERIALIZABLE
.
READ_COMMITTED
will only read committed operations to the database and maintain us safe from dirty reads.READ_UNCOMMITTED
allows the current transaction to read the uncommitted changes from another transaction. The lowest isolation level.REPEATABLE_READ
prevent dirty reads and if one row is read more than one time in a single transaction, the read result will always be the same.SERIALIZABLE
prevents non-repeatable reads, dirty reads, and phantom reads. Has impact on performance.
Propagation
We have 2 choices here:
- Use the same transaction as the parent method.
- Create a new connection and run a new transaction.
If we need the inner method running in the same transaction, we can mark it with REQUIRED
propagation level. This is the default value:
1 2 |
@Transactional(propagation = Propagation.REQUIRED) public void addMoneyToAccount(long account) { |
What does it mean?
- If a transactional context exists, use the same one.
- If there’s no transactional context, create a new one
But if we need to run the inner method in a separate transaction, we must use the REQUIRES_NEW
propagation level:
1 2 |
@Transactional(propagation = Propagation.REQUIRES_NEW) public void addMoneyToAccount(long account) { |
What happens here?
- If a transaction is running, it will be suspended, create a new transactional context, and once is successfully executed, come back to the caller context (if exists).
Each transaction opens a new connection to the database, so has direct impact on performance if you are not managing your isolation / propagation levels properly depending on your use case. For example, you don’t need
Propagation.REQUIRES_NEW
to perform read operations.
There are 5 more propagation levels:
SUPPORTS
MANDATORY
NEVER
NOT_SUPPORTED
NESTED
Rollback rules
We have seen that if a single process inside the transaction fails, then the entire transaction fails. How is this possible? Simple, Spring handles all RuntimeExceptions
thrown by a transactional context. But what if we don’t want to roll back in some cases?
We can add specific configuration to allow rollback or non-rollback depending on the exception thrown:
1 2 |
@Transactional(rollbackFor=MyException.class, noRollbackFor=OtherException.class) public void addMoneyToAccount(long account) { |
Hope this article helped you and you enjoyed it and had fun reading it.😊
Happy Learning!
Related content
Auriga: Leveling Up for Enterprise Growth!
Auriga’s journey began in 2010 crafting products for India’s