Spring Transaction Management: An unconventional guide

Last updated on October 30, 2019

You can use this guide to get an in-depth, practical understanding of how Spring's transaction management (including the @Transactional annotation) works.

The only prerequisite? You need to have a rough idea about ACID, i.e. what database transactions are and why to use them. Also, XATransactions or ReactiveTransactions are not covered here, though the general principles, in terms of Spring, still apply.

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 the 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 way better and thorough understanding, than by diving into the topic Spring-first.

How transaction management works in Java

It is of huge help to 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 realisation 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 very 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)
}
  1. You obviously need a connection to the database. DriverManager.getConnection(…​) works as well, though in most enterprise-y applications you will have a DataSource configured and get connections from that.

  2. 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 and AutoCommit(false) is the opposite: You are the master of the transaction.

  3. Let’s commit our transaction…​

  4. or rollback our changes, if there was an exception.

Yes, these 4 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, though.

(A quick note for smarty-pants: Connection pool libraries like HikariCP might toggle the autocommit mode automatically for you, depending on the configuration. But that is another topic.)

Savepoints and Isolation Levels

If you 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);
  1. This is how Spring sets isolation levels on a database connection. Not exactly rocket science, is it?

  2. 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.

If you want more practice and code samples

You’ll find a ton of code examples and exercises on plain JDBC connections and transactions in the Plain JDBC chapter of my Java database book.

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 do 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)
        }
    }
}
  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.

  2. 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 autowire 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.

document1

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();
    }
}
  1. Correct. Spring constructs a dynamic proxy of your UserService class here 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, 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)
}
  1. 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.

  2. You create a new TxManager here, which gets hold of your DataSource. Simple as.

So, let’s extend our picture from above:

document2

So to sum things up:

  1. If Spring detects @Transactional on a bean, it creates a dynamic proxy of that bean.

  2. The proxy has access to a TransactionManager and will ask it to open and close transactions / connections.

  3. The TransactionManager itself will simply do what you did in the plain Java section: "Manipulate" a JDBC connection.

@Transactional In-Depth

Now there’s 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’s 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.

Which basically means 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. If you want, 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. Which 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:

document3

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 take 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 specialised HibernateTransactionManager will make sure to:

  1. Open/close connections/transactions through Hibernate, i.e. the SessionFactory

  2. 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).

document4

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.

Fin

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.

Acknowledgements

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.

Share:

There's more where that came from

I'll send you an update when I publish new guides. Absolutely no spam, ever. Unsubscribe anytime.


Comments