skip to content

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