Ben Northrop


Decisions and software development


UnexpectedRollbackException and Spring Transaction Management


(2 comments)
April 12th 2019


I recently ran into an issue that surprised me with Spring and it's transaction management. The setup was roughly:



So the question is, of the two inserts, how many successfully go through? And what happens from the perspective of SomeClient?

The answer is nothing is committed, and an UnexpectedRollbackException is thrown back to SomeClient. If this was immediately obvious, then good for you, and continue merrily on your way. If not, read on...

The first thing to note is that the default propagation for @Transactional is REQUIRED, which means that both ServiceA and ServiceB operate within the same transactional context - i.e. it's either all or nothing with respect to commits and rollbacks.

Second, any unchecked exception that passes through any transactional boundary (e.g. @Transactional annotation) lets Spring know to mark the transaction as rollback. So, in the example above, even if exiting normally from the outer transactional boundary, Spring will, rather surreptitiously, throw the UnexpectedRollbackException to let SomeClient know that the transaction was rolled back. And this is what caught me by surprise.

At first blush, I thought that catching the RuntimeException in ServiceA and not rethrowing it through the outer transaction boundary would somehow absolve the original exception, and all would be committed. Not so...and this of course makes sense when thinking about it. If I wanted to let Spring know that this it's ok for ServiceB to throw a RuntimeException, I could set:


    @Transaction(noRollbackFor = RuntimeException.class)
    public void doSomething() {
        someDAO.insert(); // 1
 
        throw new RuntimeException();
    }

Alternatively, if I wanted ServiceB to be rolled back due to the exception, but not have that interfere with ServiceA's insert, then I could set ServiceB's propagation:



    @Transaction(propagation = REQUIRES_NEW)
    public void doSomething() {
        someDAO.insert(); // 1
 
        throw new RuntimeException();
    }

This would commit ServiceA's insert but not ServiceB's. There is also the helpful propagationBehavior called PROPAGATION_NESTED that can be set on Spring's TransactionDefinition (though note that it's not guaranteed to be supported). A great explanation from Juergen Hoeller:

PROPAGATION_NESTED is different again in that it uses a single physical transaction with multiple savepoints that it can roll back to. Such partial rollbacks allow an inner transaction scope to trigger a rollback for its scope, with the outer transaction being able to continue the physical transaction despite some operations having been rolled back. This is typically mapped onto JDBC savepoints, so will only work with JDBC resource transactions (Spring's DataSourceTransactionManager).

And there are of course more knobs and dials to Spring's Transaction Management (e.g. isolation, rollbackFor, etc.) that are important to understand before you start writing any non-trivially complex transactional code.

In the end, in terms of best practices, I'd assert that it might be a better design to implement a facade layer above the services which serves as the definitive transactional boundary, instead of just marking every Service with @Transactional and then trying to think through every exception thrown and caught between Services. This is especially true if there are quite a few services, and complicated dependencies between them. If some method definitely needs to be committed independent of the outer transaction however, then that particular method can be explicitly marked as REQUIRES_NEW. Otherwise, keep the transaction annotations on this facade and out of the service-level code, and thereby only trigger a rollback if something is thrown through this outer layer.

I'm not sure if this post will ever get seen, but I'd be interested in anyone's thoughts on best practices or something I'm missing (it's very possible!).

I believe that software development is fundamentally about making decisions, and so this is what I write about (mostly). I'm the owner of Highline Solutions and also the Principal Technical Consultant. I have two degrees from Carnegie Mellon University, most recently one in philosophy (thesis here). I live in Pittsburgh, PA with my wife and 3 energetic boys. Subscribe here or write me at ben dot northrop at gmail dot com.

Got a Comment?


Comments (2)

J.F
April 16, 2019
Hi,

I recently stumbled at the exact same problem.
But in my case I cannot use a facade pattern.
Do you have any idea if there is way to tell Spring that the outer transaction has to be committed in any way?
Ben
April 16, 2019
Hi J.F. - If you need ServiceA's insert to be committed, but not ServiceB's, then they should probably be two separate transactions, and so use REQUIRES_NEW on the inner transaction.

Alternatively, you probably could programmatically commit within ServiceA if you wanted, but this would commit everything within that transactional context (i.e. both inserts).