The Basics of Spring Framework Transactions

In this post, I will detail how Spring Framework handles transactions behind the scenes

Thiago Mendes
CodeX
7 min readMay 16, 2021

--

Image by spring.io

Before we talk about how the framework handles transactions, we need to understand some basic concepts that will be essential for understanding content throughout this post.

The first concept: The isolation of the ACID.

“If you do not know what the ACID (atomicity, consistency, isolation, durability) means, I recommend looking for associated materials because in this post I will not go into depth about this content.”

At this point, we will talk briefly about the isolation of the ACID, more specifically about read phenomena and isolation levels.

When working with concurrent transactions in a database, we need to understand what levels of isolation DBMS offer and what type of reading phenomenon each level solves.

Among these phenomena, we have:

Dirty Read: This phenomenon occurs when data can be read in a concurrent transaction even before it is committed.

Non-repeatable Read: This phenomenon occurs when a record can be read twice with different results because this record has been changed in a concurrent transaction between the two readings.

Phantom Read: This phenomenon is very similar to the Non-repeatable Read, however in this case, in one of the transactions, a query is performed with a condition that returns more than one record, while in the other transaction, we have the insertion or deletion of a record that impacts the query condition of the concurrent transaction, changing the result of the query.

Among the isolation levels, we have:

Read Uncommitted: At this level, simultaneous transactions can see uncommitted data between them, thus allowing the “dirty read” phenomenon to occur.

Read Committed: At this level, only committed data can be viewed in concurrent transactions. At this level, we no longer have the “dirty read” phenomenon, but we can still have the “non-repeatable read” and “phantom read” phenomena.

Repeatable Read: At this level, we have the guarantee that a record will be the same returned within a query in a transaction, regardless of whether it has been changed and committed in another concurrent transaction. At this level, we no longer have the occurrence of the Non-repeatable Read phenomenon, but we can still have the phantom read phenomenon.

Serializable: This is the highest level of isolation, in this scenario we avoid the occurrence of all the phenomena mentioned above, but we can have a considerable impact on performance, due to the database trying to guarantee a higher level of data integrity, by executing calls sequentially.

It is important to make it clear that everything that has been said above, related to levels of isolation, is based on what is described in the ANSI standard, but each DBMS can implement the isolation levels differently. Therefore, we may have different behaviors, depending on the DBMS used.

Another important point is that each DBMS has a default isolation level. For example, in Oracle, Postgres, and SQL Server, the default is Read Committed. In Mysql, the default is Repeatable Read.

Knowing that it is always important to read the documentation for each DBMS, to understand what levels of isolation it implements and how it handles each phenomenon.

The second concept: Spring framework proxies

This is a very important concept in general for those working with the Spring framework. Whenever you place instruction in your classes through Spring annotations, such as @Transactional, Spring needs to translate the instruction into specific blocks of code and that is when proxies appear.

Speaking specifically of the @Transactional annotation, when you annotate a method of your class with it, under the hood, Spring creates a proxy-based on your class and adds the blocks of code for opening and closing the transaction, making the call to the method of the original class in the middle of this code block that was created.

However, a very important point to keep in mind is that for these instructions that were added to the proxy to be invoked during code execution, the call to your annotated method needs to come from an external class. When the call is made to a method from within the class itself, the execution does not go through the proxy and the annotation is ignored.

But when is this proxy created and how does execution go through it?

When you inject a class that you created through @Autowired for example, Spring analyzes whether the class being injected has any type of annotation that needs to be interpreted and if so, instead of injecting your class, it injects a proxy that encapsulates all the logic of the original class.

For example, you created a class called MyService of type @Service, which has a method annotated with @Transactional that writes data to the database, called saveData.

Now let’s say you have a class of type @Controller that does the MyService class’s injection through the @Autowired annotation.

When Spring is going to inject the MyService class, instead of injecting the original class, it will inject a class called MyService$$EnhancerBySpringCGLIB for example, which will also have a saveData method. This method, however, unlike the original class method, will consist of:

NOTE: This is just a didactic example, it is not exactly the original code generated by the framework.

But, how does Spring handle transactions?

Now that we’ve taken a superficial look at these two concepts, let’s talk about how spring deals with transactions.

All the magic happens practically through the @Transactional annotation.

The @Transactional annotation can be used both at the class level and at the method level, this will depend on the desired transactional scope.

An important point to know when we use transactions in Spring is related to the rollback of the transaction. Rollbacks will only occur when the code block annotated with @Transactional throws an unchecked exception. If you want the rollback to also occur in case of checked exceptions, you need to specify the exception using the "rollbackOn" attribute of the @Transactional annotation, for example:

The @Transactional annotation has two very important attributes that we will detail the following, these attributes are isolation and propagation.

Isolation

The isolation attribute you should already imagine what it is for, based on what we saw at the beginning of the post.

You can set the isolation level in your Spring transactions, filling in the value of the isolation attribute with the possible values below:

  • @Transactional(isolation = Isolation.READ_UNCOMMITTED)
  • @Transactional(isolation = Isolation.READ_COMMITTED)
  • @Transactional(isolation = Isolation.REPEATABLE_READ)
  • @Transactional(isolation = Isolation.SERIALIZABLE)
  • @Transactional(isolation = Isolation.DEFAULT)

Each of these levels mentioned above represents the behavior of the related levels implemented in the DBMS.

If the value of this attribute is not filled in, its default value will be Isolation.DEFAULT, which consequently respects the DBMS default isolation level.

Propagation

On the other hand, the propagation attribute is related to the way that the transaction must propagate through the execution of the code.

The possible values for this attribute are:

@Transactional(propagation = Propagation.REQUIRED)

This is the default value of the propagation attribute if it is not filled. The behavior of this value is to use the current transaction if there is one, if not, a new transaction is created.

@Transactional(propagation = Propagation.SUPPORTS)

In the case of the SUPPORTS value, it uses the current transaction if there is one, if there is no transaction in progress, it does not create a new one.

@Transactional(propagation = Propagation.MANDATORY)

In MANDATORY, if there is a transaction in progress, it will be used, otherwise, Spring will throw an exception of the type IllegalTransactionStateException.

@Transactional(propagation = Propagation.NEVER)

In the case of the NEVER value, the use of a transaction will not be allowed, if there is a transaction in progress, an exception of the type IllegalTransactionStateException will be thrown.

@Transactional(propagation = Propagation.NOT_SUPPORTED)

In NOT_SUPPORTED, we have a scenario very similar to NEVER, but instead of throwing an exception, Spring will suspend the current transaction, if it exists.

@Transactional(propagation = Propagation.REQUIRES_NEW)

In the case of REQUIRES_NEW, if there is a transaction in progress, Spring will suspend it and create a new one. As soon as this new transaction ends, Spring will resume the suspended transaction.

@Transactional(propagation = Propagation.NESTED)

The NESTED value, is used when you want to work with savepoints, thus having the possibility of partial rollbacks. When there is no transaction in progress, it has the same behavior as REQUIRED and creates a new transaction.

A very important point is that to work with the NESTED value, we depend on external factors, such as the compatibility of the JDBC driver used.

You may have noticed that when we talk about the values for the propagation attribute, we talk a lot about current transactions, but what would a current transaction be?

Current transactions are nothing more than transactions coming from outside, with more comprehensive scopes, for example, let’s say you have a class of the type @Controller that uses the @Transactional annotation and this controller makes a call to another class of the type @Service that also uses the @Transactional annotation. When the call from the controller to the service occurs and the service method is invoked, there is already a current transaction coming from the controller class.

Conclusion

I hope I was able to clarify a little bit about the use of transactions in Spring and how it handles them. Knowing the correct way to use transactions can make a huge difference when it comes to scenarios with a high volume of access and concurrence. If used incorrectly, it can also compromise the performance and business rules of your application.

--

--