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