In Kotlin all exceptions are effectively unchecked. Therefore, transactions will not be rolled back for functions like this:

@Transactional
fun example(): Unit {
  runQuery()
  throw MyCheckedException()
  runQuery()
}

to achieve a correct behavior, the above code needs to be refactored as follows:

@Transactional(rollbackFor = [Exception::class])
fun example(): Unit {
  runQuery()
  throw MyCheckedException()
  runQuery()
}

this isn't very intuitive and can lead to unexpected results. Furthermore, even if a developer is aware of this, he/she should not forget to specify rollbackFor for every @Transactional annotation.

It should be possible to configure application-wide defaults for rollbackFor and noRollbackFor

Comment From: the-fine

@ilyastam as stated here: https://docs.spring.io/spring/docs/5.2.0.RC1/spring-framework-reference/data-access.html#transaction-declarative-rolling-back Transactions are rolled back by unchecked exceptions by default. So it should work out of the box with Kotlin because exceptions are unchecked https://kotlinlang.org/docs/reference/exceptions.html Do you have a sample projects that reproduces this issue?

Comment From: kilink

Kotlin doesn't distinguish between RuntimeException (unchecked) and Exception (checked), meaning Kotlin code can freely throw either type. Spring transactional support does distinguish between the two by not rolling back any checked exception by default (an exception subclassing Exception).

One can choose to only use RuntimeException derived exceptions in Kotlin to avoid that issue, but it can also arise when Kotlin code calls into a Java method that throws a checked Exception. Essentially it's a "foot gun", one mistake can lead to the unexpected behavior of a transaction not being rolled back.

Comment From: raderio

The issue is that in a transactional method we can call a method which can throw IOException, and in this case the transaction will not rollback already mutated data because IOException is not a subclass of RuntimeException

Why not to rollback by default by Exception and not by RuntimeException without any configs? This will not break the Java code.

Comment From: elab

How about specifying rollback behavior in a @Transactional annotation at the class level?

Comment From: snicoll

@elab that would mark all public methods in the class to be transactional.

Comment From: Fryie

It would be really beneficial to have this as a configurable option (maybe even set by default when choosing Kotlin on start.spring.io). Since Kotlin doesn't force you to handle checked exceptions, it's easy to accidentally end up with a corrupted state because the transaction is not rolled back.

Comment From: kilink

Here is what we did to work around the issue in our Kotlin Spring Boot app:


import org.springframework.beans.factory.config.BeanDefinition
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Role
import org.springframework.core.KotlinDetector.isKotlinType
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource
import org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration
import org.springframework.transaction.interceptor.DelegatingTransactionAttribute
import org.springframework.transaction.interceptor.TransactionAttribute
import org.springframework.transaction.interceptor.TransactionAttributeSource
import java.lang.reflect.AnnotatedElement
import java.lang.reflect.Method

@Configuration
class TransactionConfig : ProxyTransactionManagementConfiguration() {
    /**
     * Define a custom [TransactionAttributeSource] that will roll back transactions
     * on checked Exceptions if the annotated method or class is written in Kotlin.
     *
     * Kotlin doesn't have a notion of checked exceptions, but [Transactional] assumes
     * Java semantics and does *not* roll back on checked exceptions. This can become
     * an issue if a Kotlin class explicitly throws Exception or calls into a Java
     * method which throws checked exceptions.
     *
     * @see: https://github.com/spring-projects/spring-framework/issues/23473
     */
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    override fun transactionAttributeSource(): TransactionAttributeSource {
        return object : AnnotationTransactionAttributeSource() {
            override fun determineTransactionAttribute(element: AnnotatedElement): TransactionAttribute? {
                val txAttr = super.determineTransactionAttribute(element)
                    ?: return null
                val isKotlinClass = when (element) {
                    is Class<*> -> isKotlinType(element)
                    is Method -> isKotlinType(element.declaringClass)
                    else -> false
                }
                if (isKotlinClass) {
                    return object : DelegatingTransactionAttribute(txAttr) {
                        override fun rollbackOn(ex: Throwable): Boolean {
                            return super.rollbackOn(ex) || ex is Exception
                        }
                    }
                }
                return txAttr
            }
        }
    }
}

Essentially extremely cautious, and only rolls back for Exception by default if the annotated class is written in Kotlin. I feel this fix could be rolled into spring-tx, as it seems pretty surgical / low risk (only change behavior for Kotlin classes).

Comment From: snicoll

The config infrastructure could be shared by work on https://github.com/spring-projects/spring-framework/issues/24291

Comment From: quaff

kotlin return object : AnnotationTransactionAttributeSource() {

It should be return object : AnnotationTransactionAttributeSource(false) to align with spring 6.0, other people may copy your code, could you edit it? @kilink

Comment From: jhoeller

I'm introducing a rollbackOn attribute on @EnableTransactionManagement, with an enum of RUNTIME_EXCEPTIONS and ALL_EXCEPTIONS. This makes it easier to document the behavior and the recommendations, and also easier to consistently support it for Spring's @Transactional as well as JTA's jakarta.transaction.Transactional when used with Spring, while not exposing the full complexity of rollback rules and no-rollback rules on @EnableTransactionManagement itself.

For any further customization needs at the global level, AnnotationTransactionAttributeSource provides an addDefaultRollbackRule(RollbackRuleAttribute) method for arbitrary default rollback rules.