skip to content

TransactionalOperator

TransactionalOperator wraps a Mono or Flux so the transaction commits on completion and rolls back on an error signal, with state carried in the Reactor Context. The reactive counterpart of TransactionTemplate, and a natural follow-up once you mention WebFlux.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is TransactionalOperator and why can't you just use @Transactional / a ThreadLocal-based transaction in reactive (WebFlux/R2DBC) code?

level: juniorimportance: must knowfreq 70%

answer

  1. ThreadLocal dies when threads hop
  2. Reactor Context carries the tx, not the thread
  3. reactive twin of TransactionTemplate
  4. built from ReactiveTransactionManager
  5. complete=commit, error=rollback

basics

~20 s

TransactionalOperator is a helper that wraps a Mono or Flux in a transaction. Reactive code hops threads, so the classic ThreadLocal transaction doesn't work; the operator carries the transaction in the Reactor Context instead. It commits on success, rolls back on error.

solid answer

~40 s

TransactionalOperator is the programmatic transaction helper for reactive code — the reactive analogue of TransactionTemplate. You build one from a ReactiveTransactionManager (e.g. R2dbcTransactionManager) and call operator.transactional(myMono) or operator.transactional(myFlux) to wrap a pipeline. It begins a transaction, subscribes to your publisher, commits when the publisher completes normally, and rolls back if it emits an error signal. It exists because imperative transaction management (both @Transactional and TransactionTemplate) binds the connection to a ThreadLocal, and reactive execution freely switches threads between operators — a ThreadLocal set on the subscribing thread is invisible downstream. Reactive Spring instead stores the transaction resources in the Reactor subscriber Context, which travels with the data flow regardless of which thread runs each stage. @Transactional also works reactively when the method returns a Publisher, but TransactionalOperator gives explicit, programmatic control.

code

java · 22 lines
java
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Mono;

public class AccountService {

    private final TransactionalOperator rxtx;
    private final AccountRepository accounts; // R2DBC reactive repository

    public AccountService(ReactiveTransactionManager txm, AccountRepository accounts) {
        // TransactionalOperator.create wraps a ReactiveTransactionManager
        this.rxtx = TransactionalOperator.create(txm);
        this.accounts = accounts;
    }

    public Mono<Void> transfer(long from, long to, long amount) {
        Mono<Void> work = accounts.debit(from, amount)
                .then(accounts.credit(to, amount));
        // commit on completion, rollback if debit/credit errors
        return rxtx.transactional(work);
    }
}

go deeper

for a junior

Know it wraps a Mono/Flux, commits on success, rolls back on error, and exists because ThreadLocal transactions don't survive thread hops.

for a middle

Explain the Reactor Context as the ThreadLocal replacement and name R2dbcTransactionManager / ReactiveTransactionManager.

for a senior

Contrast programmatic TransactionalOperator with annotation-driven @Transactional and know both share the reactive machinery.

for a principal

Discuss why context-propagation (not ThreadLocal) is the correct model for reactive, and the constraint that only reactive data access enlists.

## The problem it solves In a blocking Spring app, a transaction is a **ThreadLocal** thing: `PlatformTransactionManager` opens a JDBC connection, binds it to the current thread via `TransactionSynchronizationManager`, and every DAO call on that thread transparently finds it. `@Transactional` and `TransactionTemplate` both rely on this. Reactive code breaks that assumption. A `Mono`/`Flux` pipeline is assembled once but executed later, and different operators can run on **different threads** (thread hops via schedulers, event-loop handoffs). A value bound to the thread that started the pipeline is not visible to a stage running on another thread. So ThreadLocal-based transactions cannot work. ## What TransactionalOperator is `org.springframework.transaction.reactive.TransactionalOperator` is the **programmatic** reactive transaction helper — the reactive counterpart of `TransactionTemplate`. It is backed by a `ReactiveTransactionManager` (the reactive counterpart of `PlatformTransactionManager`), such as `R2dbcTransactionManager` or `ReactiveMongoTransactionManager`. You create it once: ```java TransactionalOperator rxtx = TransactionalOperator.create(reactiveTxManager); ``` and use it to wrap a pipeline: ```java rxtx.transactional(service.doWork()); // Mono<T> or Flux<T> in, same type out ``` ## How the transaction travels Reactive Spring stores the transaction (the R2DBC `Connection`, the transaction status) in the **Reactor Context** — specifically in a `TransactionContext` held under the subscriber context. The Reactor Context flows **upstream** with the subscription and is available to every operator in the chain, no matter which thread executes it. That is the reactive replacement for ThreadLocal. R2DBC repositories/`DatabaseClient` participating in the same reactive chain read that context and enlist in the transaction automatically. ## Lifecycle - On subscribe: the operator asks the `ReactiveTransactionManager` for a transaction, which puts a `TransactionContext` into the Reactor Context. - Your wrapped publisher runs inside that context. - **Complete** (onComplete) → **commit**. - **Error** (onError) → **rollback**, then the error is re-propagated to your subscriber. - **Cancel** → rollback (the subscription was cut short). ## Relationship to @Transactional `@Transactional` also works on reactive methods **as long as the method returns a `Publisher`** (`Mono`/`Flux`) and a `ReactiveTransactionManager` bean is present. Under the hood it uses the same reactive machinery. `TransactionalOperator` is chosen when you want **explicit, programmatic** control — e.g. wrapping only part of a flow, or dynamically deciding rollback via `setRollbackOnly()`. ## Gotchas to know early - A blocking JDBC call (`JdbcTemplate`) inside a reactive pipeline is **not** enlisted — you need a reactive data access API (R2DBC, reactive Mongo). - If you break out of the reactive chain (e.g. `.block()`, imperative side effects), the transaction context is lost. - You must **return** the wrapped publisher and let the framework subscribe; if nothing subscribes, nothing runs.

  • Which transaction manager would you inject for R2DBC?
    R2dbcTransactionManager (a ReactiveTransactionManager). For blocking JDBC you'd use DataSourceTransactionManager, but that is imperative and won't participate in a reactive pipeline.
  • Does @Transactional work at all in WebFlux?
    Yes, provided the method returns a Publisher (Mono/Flux) and a ReactiveTransactionManager bean exists. It uses the same Reactor-Context machinery. It does not work on methods returning plain values in a reactive context.

saying these in an interview costs you the question

  • Claiming @Transactional never works in reactive apps
  • Saying the transaction is stored in a ThreadLocal like in Spring MVC
  • Thinking a blocking JdbcTemplate call inside the Mono joins the reactive transaction

context

open as a page

With operator.transactional(mono/flux), exactly which reactive signal triggers a commit and which triggers a rollback? What happens on cancellation?

level: middleimportance: must knowfreq 60%

basics

~10 s

It commits when the wrapped publisher completes normally (onComplete). It rolls back if the publisher emits an error (onError). If the subscription is cancelled before completing, it also rolls back.

open as a page

How does the reactive transaction propagate through the pipeline, and what operations will silently NOT participate in the transaction?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The transaction (its connection) is stored in the Reactor subscriber Context and flows to every operator in the same reactive chain. Anything that leaves that chain — a blocking JDBC call, work on a separate subscription, or code after .block() — does not join the transaction.

open as a page

How does the execute(TransactionCallback) form of TransactionalOperator differ from transactional(Mono/Flux), and how do you force a rollback on an otherwise-successful flow?

level: seniorimportance: should knowfreq 40%

basics

~20 s

transactional() just wraps an existing Mono/Flux. execute() takes a callback that receives the ReactiveTransaction handle, so you can inspect it and call setRollbackOnly() to force a rollback even when the pipeline completes successfully. execute() returns a Flux.

open as a page

What are the default rollback rules and propagation limitations of the reactive transaction model compared to the imperative one, and what would you watch for at scale?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

A programmatic TransactionalOperator built with a plain TransactionDefinition rolls back on any error signal. Reactive supports common propagations like REQUIRED but not savepoint-based NESTED on R2DBC. At scale, watch long-open transactions from big Flux streams holding connections.

open as a page