Spring Framework offers two ways to programmatically manage transactions - using:

  • TransactionTemplate or TransactionalOperator.

  • The TransactionManager implementation itself.

The Spring team usually recommends TransactionTemplate for software transaction management in imperative flows and TransactionalOperator for reactive code. The second approach is similar to using the UserTransaction API from JTA, although exception handling is less overloaded.

Using the TransactionTemplate

Template TransactionTemplate takes the same approach as other Spring templates, such as JdbcTemplate. It uses a callback approach (to free application code from having to do the stereotypical acquisition and release of transactional resources) and entails creating code that is intent-driven, i.e. your code's work is focused solely on what you need to accomplish.

As the following examples show, using a TransactionTemplate is completely binding you to the transaction infrastructure and Spring API. Whether programmatic transaction management is appropriate for your development needs is a matter for you to decide for yourself.

Application code that should run in a transactional context and that explicitly uses TransactionTemplate is similar to the following example. You, as an application developer, can write a TransactionCallback implementation (usually expressed as an anonymous inner class) containing the code to be executed in the context of a transaction. You can then pass an instance of your custom TransactionCallback to the execute(..) method exposed to the TransactionTemplate. The following example shows how to do this:

Java

public class SimpleService implements Service {
    // a single TransactionTemplate, common to all methods of this instance
    private final TransactionTemplate transactionTemplate;
    // use dependency injection via the constructor to provide a PlatformTransactionManager
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }
    public Object someServiceMethod() {
        return transactionTemplate.execute(new TransactionCallback() {
            // the code in this method is executed in the context of a transaction
            public Object doInTransaction(TransactionStatus status) {
                updateOperation1();
                return resultOfUpdateOperation2();
            }
        });
    }
}
Kotlin

// use dependency injection via constructor to provide PlatformTransactionManager
class SimpleService(transactionManager: PlatformTransactionManager) : Service {
    // a single TransactionTemplate, common to all methods of this instance
    private val transactionTemplate = TransactionTemplate(transactionManager)
    fun someServiceMethod() = transactionTemplate.execute<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

If there is no return value, you can use a helper class TransactionCallbackWithoutResult with an anonymous class like this is shown below:

Java

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        updateOperation1();
        updateOperation2();
    }
});
Kotlin

transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        updateOperation1()
        updateOperation2()
    }
})

The code inside the callback can roll back the transaction by calling the setRollbackOnly() method for the provided TransactionStatus object, as shown below:

Java

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        try {
            updateOperation1();
            updateOperation2();
        } catch (SomeBusinessException ex) {
            status.setRollbackOnly();
        }
    }
});
Kotlin

transactionTemplate.execute(object : TransactionCallbackWithoutResult() {
    override fun doInTransactionWithoutResult(status: TransactionStatus) {
        try {
            updateOperation1()
            updateOperation2()
        } catch (ex: SomeBusinessException) {
            status.setRollbackOnly()
        }
    }
})

Task transaction parameters

Transaction parameters (such as propagation mode, isolation level, timeout, and so on) can be set for the TransactionTemplate programmatically or in configuration. By default, TransactionTemplate instances have standard transaction settings. The following example shows programmatic configuration of transaction parameters for a specific TransactionTemplate:

Java

public class SimpleService implements Service {
    private final TransactionTemplate transactionTemplate;
    public SimpleService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        // if desired, you can explicitly set the transaction parameters here
        this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        this.transactionTemplate.setTimeout(30); // 30 seconds
        // and so on...
    }
}
Kotlin

class SimpleService(transactionManager: PlatformTransactionManager) : Service {
    private val transactionTemplate = TransactionTemplate(transactionManager).apply {
        // if desired, you can explicitly set transaction parameters here
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED timeout = 30 // 30 seconds
        // and so on...
    }
}

In the following example, TransactionTemplate is defined with some custom transaction settings using XML configuration in Spring:


<bean id="sharedTransactionTemplate"
      class="org.springframework.transaction.support.TransactionTemplate">
    <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
    <property name="timeout" value="30"/>
</bean>

You can then inject sharedTransactionTemplate into as many services as needed.

Finally, instances of the TransactionTemplate class are thread-safe because instances do not retain any dialog state. However, TransactionTemplate instances retain configuration state. So, although multiple classes can share a single TransactionTemplate instance, if you want a class to use a TransactionTemplate with different settings (for example, different isolation levels), you must create two different ones instance of TransactionTemplate.

Using TransactionalOperator

TransactionalOperator follows the programming structure of the operator, which is similar to others reactive operators. It uses a callback approach (to free application code from having to do the stereotypical acquisition and release of transactional resources) and entails creating code that is intent-driven, i.e. your code's work is focused solely on what you need to accomplish.

As the following examples show, using TransactionalOperator is completely binding you to the transaction infrastructure and Spring API. Whether programmatic transaction management is suitable for your development needs is a matter for you to decide for yourself.

Application code that needs to be executed in the context of a transaction that explicitly uses the TransactionalOperator is similar to that which is represented in the following example:

Java

public class SimpleService implements Service {
    // a single TransactionalOperator, common to all methods of this instance
    private final TransactionalOperator transactionalOperator;
    // use constructor dependency injection to provide a ReactiveTransactionManager
    public SimpleService(ReactiveTransactionManager transactionManager) {
        this.transactionalOperator = TransactionalOperator.create(transactionManager);
    }
    public Mono<Object> someServiceMethod() {
        // the code in this method is executed in the context of a transaction
        Mono<Object> update = updateOperation1();
        return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
    }
}
Kotlin

// use dependency injection via constructor to provide ReactiveTransactionManager
class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
    // a single TransactionalOperator, common to all methods of this instance
    private val transactionalOperator = TransactionalOperator.create(transactionManager)
    suspend fun someServiceMethod() = transactionalOperator.executeAndAwait<Any?> {
        updateOperation1()
        resultOfUpdateOperation2()
    }
}

TransactionalOperator can be used in two ways:

  • Based on an operator using types from Project Reactor (mono.as(transactionalOperator::transactional))

  • Based on a callback for all other cases (transactionalOperator.execute(TransactionCallback<T>))

The code inside the callback can roll back the transaction by calling the setRollbackOnly() method on the provided ReactiveTransaction object as shown below:

Java

transactionalOperator.execute(new TransactionCallback<>() {
    public Mono<Object> doInTransaction(ReactiveTransaction status) {
        return updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
        }
    }
}); 
Kotlin

transactionalOperator.execute(object : TransactionCallback() {
    override fun doInTransactionWithoutResult(status: ReactiveTransaction) {
        updateOperation1().then(updateOperation2)
                    .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly())
    }
})

Cancel signals

In Reactive Streams, a Subscriber can cancel its Subscription and stop its Publisher. Operators in Project Reactor, as well as in other libraries, such as next(), take(long), timeout(Duration) and others, Cancellations may be made. It is impossible to find out the reason for cancellation, whether it was an error or simply a lack of interest in further consumption. Since version 5.3, cancellation signals result in a rollback. As a result, it is important to consider the operators used in descending order from Publisher transactions. Particularly in the case of Flux or other multi-valued Publisher, the full output must be consumed for the transaction to complete.

Setting transaction parameters

You can set transaction parameters (such as propagation mode, isolation level, timeout, etc.) for TransactionalOperator. By default, TransactionalOperator instances have standard transactional settings. The following example shows how to configure transactional parameters for a specific TransactionalOperator:

Java

public class SimpleService implements Service {
    private final TransactionalOperator transactionalOperator;
    public SimpleService(ReactiveTransactionManager transactionManager) {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        // if desired, you can explicitly set the transaction parameters here
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
        definition.setTimeout(30); // 30 seconds
        // and so on...
        this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
    }
}
Kotlin

class SimpleService(transactionManager: ReactiveTransactionManager) : Service {
    private val definition = DefaultTransactionDefinition().apply {
        // if desired, you can explicitly set the transaction parameters here
        isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED timeout = 30 // 30 seconds
        // and so on...
    }
    private val transactionalOperator = TransactionalOperator(transactionManager, definition)
}

Using TransactionManager

The following sections cover programmatic use of imperative and reactive transaction managers.

Using PlatformTransactionManager

For imperative transactions, you can use org.springframework.transaction.PlatformTransactionManager to manage the transaction directly. To do this, pass the implementation of the PlatformTransactionManager you are using to your bean via a bean reference. Then, using the TransactionDefinition and TransactionStatus objects, transactions can be initiated, rolled back, and committed. The following example shows how to do this:

Java

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicit setting of the transaction name can only be done programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
    // put your business logic here
} catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);
Kotlin

val def = DefaultTransactionDefinition()
// explicitly setting the name transactions can only be done programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
val status = txManager.getTransaction(def)
try {
    // put your business logic here
} catch (ex: MyException) {
    txManager.rollback (status)
    throw ex
}
txManager.commit(status)

Using ReactiveTransactionManager

When working with With reactive transactions, you can use org.springframework.transaction.ReactiveTransactionManager to manage the transaction directly. To do this, pass the implementation of the ReactiveTransactionManager you are using to your bean via a bean reference. Then, using the TransactionDefinition and ReactiveTransaction objects, you can initiate, rollback, and commit transactions. The following example shows how to do this:

Java

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicit setting of the transaction name can only be done programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);
reactiveTx.flatMap(status -> {
    Mono<Object> tx = ...; // put your business logic here
    return tx.then(txManager.commit(status))
            .onErrorResume(ex -> txManager.rollback( status).then(Mono.error(ex)));
});
Kotlin

val def = DefaultTransactionDefinition()
// Explicit setting of the transaction name can only be done programmatically
def.setName("SomeTxName")
def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED
val reactiveTx = txManager.getReactiveTransaction(def)
reactiveTx.flatMap { status ->
    val tx = ... // put your business logic here
    tx.then(txManager.commit(status))
            .onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) }