Spring Transaction Management: An Unconventional Guide
You can use this guide to as an in-depth, practical understanding of how Spring's transaction management (including the @Transactional
annotation) works.
The only prerequisite? You need to have a general idea of ACID, i.e. what database transactions are and when to use them. Also, XATransactions, or ReactiveTransactions, are not covered here, though the general principles, in terms of Spring, still apply.
You may also like: Effective Spring Transaction Management
Introduction
As opposed to, say, the official Spring documentation, this guide won’t confuse you with terms like physical or logical transactions.
Instead, you are going to learn Spring transaction management in an unconventional way — from the ground up, step by step.
Why? It might take you a bit longer to read and finish this guide, but at the end, you’ll have a much better and thorough understanding than if you had dived into the topic Spring-first.
Let's get started.
How Transaction Management Works in Java
First things first, it is crucial that you understand how database transactions work with plain Java (JDBC). If you are thinking of skipping this section, DON'T!
Starting, Committing, and Rolling Back Transactions
The first realization is this: It does not matter if you are using Spring’s @Transactional
annotation, plain Hibernate, jOOQ, or any other database library.
In the end, they all do the same thing to open and close a database transaction, which is this:
Connection connection = dataSource.getConnection(); // (1)
try (connection) {
connection.setAutoCommit(false); // (2)
// execute some SQL statements...
connection.commit(); // (3)
} catch (SQLException e) {
connection.rollback(); // (4)
}
- You obviously need a connection to the database.
DriverManager.getConnection(…)
works as well, though, in most enterprise-esque applications, you will have aDataSource
configured and can get connections from that. - This is the only way to start a database transaction in Java, even though the name might sound a bit off.
AutoCommit(true)
wraps every single SQL statement in its own transaction, andAutoCommit(false)
is the opposite: You are the master of the transaction. - Let’s commit our transaction…
- Or rollback our changes, if there was an exception.
Yes, these four lines are (oversimplified) everything that Spring does whenever you are using the @Transactional
annotation. In the next chapter, you’ll find out how that works. Before we go there, there’s a tiny bit more you need to learn.
(A quick note for smarty-pants: Connection pool libraries like HikariCP might toggle the auto-commit mode automatically for you, depending on the configuration. But that is another topic I won't get into for the time being.)
Savepoints and Isolation Levels
If you've already played with Spring’s @Transactional
annotation, you might have encountered something like this:
@Transactional(propagation=TransactionDefinition.NESTED,
isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED)
We will cover nested Spring transactions and isolation levels a bit later, but again, it helps to know what these parameters all boil down to the following JDBC code:
connection.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // (1)
Savepoint savePoint = connection.setSavepoint(); // (2)
...
connection.rollback(savePoint);
- This is how Spring sets isolation levels on a database connection. Not exactly rocket science, is it?
- Nested transactions in Spring are really just JDBC savepoints. If you don’t know what a savepoint is, have a look at this tutorial, for example. Note that savepoint support is dependent on your JDBC driver/database.
Looking for more practice? You can find a ton of code examples and exercises on plain JDBC connections and transactions in the Plain JDBC chapter of my Java database book. Check it out!
How Transaction Management Works in Spring
As you now have a good JDBC transaction foundation, let’s have a look at Spring.
Spring usually offers more than one way to skin a cat. The same goes for transaction management.
Legacy Transaction Management: XML
Back in the day, when XML configuration was the norm for Spring projects, you could also configure transactions directly in XML. Apart from a couple of legacy enterprise projects, you won’t find this approach anymore in the wild, as it has been superseded with the much simpler @Transactional
annotation.
Hence, we will skip XML configuration in this guide, but here’s a quick glimpse of what it looked like (taken straight from the official Spring documentation):
<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
Interested in why it is called tx:advice
? This is because Spring is using AOP (Aspect-Oriented Programming) to perform transactions. You can read more about AOP in the official Spring documentation.
Spring’s @Transactional Annotation
Now, let’s have a look at what modern Spring transaction management usually looks like:
public class UserService {
@Transactional
public void registerUser(User user) {
//...validate the user
userDao.save(user);
}
}
As long as you have the @EnableTransactionManagement
annotation set on a Spring configuration (and a couple other beans configured — more on that in a second), you can annotate your methods with the @Transactional
annotation and be sure that your method executes inside a database transaction.
What does "executing inside a database transaction" really mean? Armed with the knowledge from the previous section, the code above translates (simplified) directly to this:
public class UserService {
public void registerUser(User user) {
Connection connection = dataSource.getConnection(); // (1)
try (connection) {
connection.setAutoCommit(false); // (1)
//...validate the user
userDao.save(user); // (2)
connection.commit(); // (1)
} catch (SQLException e) {
connection.rollback(); // (1)
}
}
}
- This is all just standard opening and closing of a JDBC connection. See the previous section. That’s what Spring’s transactional annotation does for you automatically, without you having to write it.
- This is your own code, saving the user through a DAO.
This example might look a bit oversimplified, but let’s have a look at how Spring magically inserts this connection/transaction code for you.
Proxies
Spring cannot really rewrite your Java class, like I did above, to insert the connection code. Your registerUser()
method really just calls userDao.save(user)
; there’s no way to change that on the fly.
But Spring has an advantage. At its core, it is an IoC container. It instantiates a UserService
for you and makes sure to auto-wire that UserService
into any other bean that needs a UserService
.
Now, whenever you are using @Transactional
on a bean, Spring uses a tiny trick. It does not just instantiate a UserService
, but also a transactional proxy of that UserService
. Let’s see this in a picture.
As you can see from that diagram, the proxy has one job.
- Opening and closing database connections/transactions.
- And then delegating to the real
UserService
. - And other beans, like your
UserRestController,
will never know that they are talking to a proxy, and not the real thing.
Quick Exam: Have a look at the following source code and tell me what kind of UserService
Spring automatically constructs, assuming it is marked with @Transactional
or has a @Transactional
method.
@Configuration
@EnableTransactionManagement
public static class MyAppConfig {
@Bean
public UserService userService() { // (1)
return new UserService();
}
}
- Correct. Here, Spring constructs a dynamic proxy of your
UserService
class that can open and close database transactions for you. A proxy-through-subclassing with the help of the Cglib library. There are also other ways to construct proxies, but let’s leave it at that for the moment.
PlatformTransactionManager
Now there’s only one crucial piece of information missing. Your UserService
gets proxied on the fly, and the proxy opens up and closes connections/transactions for you.
But that means the proxy needs a DataSource
: To get a connection, commit, and/or rollback, close the connection, etc. In Spring, there’s a fancy name for the interface handling all that transaction state, which is called PlatformTransactionManager.
There are different implementations, but the simplest one is a DataSourceTransactionManager,
and armed with your knowledge from the plain Java chapter, you’ll know exactly what it does. First, let’s look at the Spring configuration:
@Bean
public DataSource dataSource() {
return null; // (1)
}
@Bean
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource()); // (2)
}
- Returning null here, obviously, doesn’t make sense but is put here for brevity. You’d rather create a MySQL, Postgres, Oracle, or connection pooling
DataSource
. - You create a new
TxManager
here, which gets hold of yourDataSource
. Simple.
So, let’s extend our picture from above:
So to sum things up:
- If Spring detects
@Transactional
on a bean, it creates a dynamic proxy of that bean. - The proxy has access to a
TransationManager
and will ask it to open and close transactions/connections. - The
TransactionManager
itself will simply do what you did in the plain Java section: "Manipulate" a JDBC connection.
@Transactional In-Depth
Now, there are a couple of interesting use cases when it comes to Spring’s transactional annotation.
Let’s have a look at 'physical' vs 'logical' transactions.
Physical Vs. Logical
Imagine the following two transactional classes:
@Service
public class UserService {
@Autowired
private InvoiceService invoiceService;
@Transactional
public void invoice() {
invoiceService.createPdf();
// send invoice as email, etc.
}
}
@Service
public class InvoiceService {
@Transactional
public void createPdf() {
// ...
}
}
UserService
has a transactional invoice()
method, which calls another transactional method, createPdf()
, on the InvoiceService
.
Now, in terms of database transactions, this should really just be one database transaction. (Remember: getConnection(). setAutocommit(false). commit()
) Spring calls this physical transaction, even though this might sound a bit confusing.
From Spring’s side, however, there are two logical pieces of the transaction: first in UserService
, the other one in InvoiceService
. Spring has to be smart enough to know that both @Transactional
methods should use the same underlying database transaction. That’s it.
How would things be different with the following change to InvoiceService
?
@Service
public class InvoiceService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
Now you are telling Spring that createPDF()
needs to execute in its own transaction. Thinking back to the plain Java section of this guide, did you see a way to "split" a transaction in half? Neither did I.
This basically means that your code will open two (physical) connections/transactions to the database. (Again: getConnection()x2. setAutocommit(false)x2. commit()x2
) Spring now has to be smart enough that the two logical transaction pieces (invoice()
/ createPdf()
) now also map to two different database transactions.
Which brings us to propagation modes:
Propagation Modes
When looking at the Spring source code, you’ll find a variety of propagation modes that you can plug into the @Transactional
method.
- REQUIRED(0)
- SUPPORTS(1)
- MANDATORY(2)
- REQUIRES_NEW(3)
- NOT_SUPPORTED(4)
- NEVER(5)
- NESTED(6)
Exercise:
In the plain Java section, I showed you everything that JDBC can do when it comes to transactions. Take a minute to think about what every single Spring propagation mode at the end REALLY does to your DataSource
, or rather your JDBC connection.
Then have a look at the following answers:
- Required (default): My method needs a transaction, either open one for me or use an existing one —
getConnection(). setAutocommit(false). commit()
. - Supports: I don’t really care if a transaction is open or not, I can work either way — nothing to do with JDBC.
- Mandatory: I’m not going to open up a transaction myself, but I’m going to cry if no one else opened one up — nothing to do with JDBC.
- Require_new: I want my completely own transaction —
getConnection(). setAutocommit(false). commit()
. - Not_Supported: I really don’t like transactions, I will even try and suspend a current, running transaction — nothing to do with JDBC.
- Never: I’m going to cry if someone else started up a transaction — nothing to do with JDBC.
- Nested: It sounds so complicated, but we are really just talking savepoints—
connection.setSavepoint().
As you can see, most propagation modes really have nothing to do with the database or JDBC, but more with how you structure your program with Spring and how/when/where you expect transactions to be there.
If you want to practice Spring transactions...
It takes a while and a lot of practice to really grasp all the different transaction propagation modes and what effects they have in a Spring application.You’ll find a ton of code examples and exercises on Spring transactions in my Java database book.
Isolation Levels
This is almost a trick question at this point, but what happens when you configure the @Transactional
annotation like so:
@Transactional(isolation = Isolation.REPEATABLE_READ)
Yes, it does simply lead to this:
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
You can read more about isolation levels here, but be advised that when it comes to isolation levels or even switching them in a transaction, you must make sure to consult with your JDBC driver/database to understand what is supported and what not.
The Most Common @Transactional Pitfall
There is one pitfall that Spring beginners usually run into. Have a look at the following code:
@Service
public class UserService {
@Transactional
public void invoice() {
createPdf();
// send invoice as email, etc.
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createPdf() {
// ...
}
}
You have a UserService
class with a transactional invoice method that calls createPDF()
, which is also transactional.
How many physical transactions would you expect to be open once someone calls invoice()
?
Nope, the answer is not two, but one. Why?
Let’s go back to the proxies' section of this guide. Spring injects that transactional proxy for you, but once you are inside the UserService
class and call other inner methods, there is no more proxy involved — no new transaction for you.
Let’s have a look at it with a picture:
There’s some tricks (like self-injection), which you can use to get around this limitation. But the main takeaway is: Always keep the proxy transaction boundaries in mind.
How Spring + Hibernate Transaction Management Works
At some point, you will want your Spring application to integrate with another database library, such as Hibernate, Jooq, etc. Let’s look at Hibernate as an example.
Imagine you have a @Transactional
Spring method and are using it with a DataSourcePlatformTransactionManager
, like the one discussed in the previous section. Spring will, therefore, open and close connections on that DataSource
for you.
But if your code calls Hibernate, Hibernate itself will end up calling its own SessionFactory
to create and close new sessions (~= connections) and manage their state without Spring. So, Hibernate won’t know about any existing Spring transactions.
What’s the fix?
There is a simple (for the end-user) workaround: Instead of using a DataSourcePlatformTransactionManager in your Spring configuration, you will be using a HibernateTransactionManager or JpaTransactionManager.
The specialized HibernateTransactionManager
will make sure to:
- Open/close connections/transactions through Hibernate, i.e. the
SessionFactory
- Be smart enough to allow you to use that very same connection/transaction in non-Hibernate, i.e. plain JDBC code
As always, a picture might be simpler to understand (though note, the flow between the proxy and real service is only conceptually right and oversimplified).
That is, in a nutshell, how you integrate Spring and Hibernate. For other integrations or a more in-depth understanding, it helps to have a quick look at all possible PlatformTransactionManager implementations that Spring offers.
Conclusion
By now, you should have a pretty good overview of how transaction management works with the Spring framework. The biggest takeaway should be that it does not matter which framework you are using in the end, it is all about the basics.
Get them right (Remember: getConnection(). setAutocommit(false). commit()
.), and you will have a much easier understanding of what happens later on in your complex, "enterprise" application.
Thanks for reading!
Acknowledgments
Thanks to Andreas Eisele for feedback on the early versions of this guide. Thanks to Ben Horsfield for coming up with much-needed JavaScript snippets to enhance this guide.
Further Reading
Effective Spring Transaction Management
How Does Spring's @Transactional Really Work?
Understanding Transaction Propagation