== rollback behaviour == REQUIRED inner, outer rolls back outcome : IllegalStateException message : outer failed after the inner call returned rows : 0 -> inner work rolled back REQUIRES_NEW inner, outer rolls back outcome : IllegalStateException message : outer failed after the inner call returned rows : 1 -> inner work SURVIVED NESTED inner, outer rolls back outcome : NestedTransactionNotSupportedException message : Transaction manager does not allow nested transactions by default - specify 'nestedTransactionAllowed' property with value 'true' rows : 0 -> inner work rolled back REQUIRED inner throws, outer catches it outcome : UnexpectedRollbackException message : Transaction silently rolled back because it has been marked as rollback-only rows : 0 -> inner work rolled back REQUIRES_NEW inner throws, outer catches it outcome : returned normally rows : 0 -> inner work rolled back NESTED inner throws, outer catches it outcome : returned normally rows : 0 -> inner work rolled back The third and fourth rows are the ones worth sitting with. When a REQUIRED inner scope throws, it marks the SHARED transaction rollback-only before the exception leaves it. The caller can catch the exception -- and does, and returns normally -- but the transaction is already doomed, so the commit at the end throws UnexpectedRollbackException. Catching the exception did not save the work; it only moved the failure to a place with no useful stack trace. With REQUIRES_NEW the inner scope had its own physical transaction, so its rollback is contained and the caller's catch behaves the way the code reads.