skip to content

ReactiveTransactionManager

Reactive transactions cannot use ThreadLocals, so the state rides in the Reactor Context via ReactiveTransactionManager and R2DBC. A precise way to test whether you understand why blocking-era assumptions break in WebFlux.

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

questions

5

What is ReactiveTransactionManager and how does it differ from PlatformTransactionManager?

level: juniorimportance: must knowfreq 55%

answer

  1. Two interfaces: Platform (blocking) vs Reactive (Mono)
  2. getReactiveTransaction/commit/rollback return Mono
  3. R2dbcTransactionManager over ConnectionFactory
  4. state in Reactor Context, not ThreadLocal
  5. @Transactional picks path by return type

basics

~10 s

ReactiveTransactionManager runs transactions for non-blocking code, returning Mono results instead of blocking. PlatformTransactionManager is the classic blocking one for imperative code (JDBC/JPA). You pick the one matching your stack.

solid answer

~40 s

Spring has two transaction-manager abstractions. PlatformTransactionManager is the imperative, blocking one (DataSourceTransactionManager, JpaTransactionManager); its methods return synchronously and it holds transaction state in ThreadLocal. ReactiveTransactionManager is its non-blocking sibling for reactive stacks: getReactiveTransaction, commit, and rollback all return Mono, so nothing blocks the event-loop thread. Its main implementation is R2dbcTransactionManager over an R2DBC ConnectionFactory. The key difference beyond return types is where transaction state lives: reactive managers carry it in the Reactor Context rather than a thread-bound ThreadLocal, so the transaction survives when work hops between threads. @Transactional works with either — Spring inspects whether the method returns a reactive type (Mono/Flux) to choose the reactive path.

code

java · 16 lines
java
@Configuration
@EnableTransactionManagement
class TxConfig {

    // Reactive: state carried in the Reactor Context
    @Bean
    ReactiveTransactionManager reactiveTx(ConnectionFactory cf) {
        return new R2dbcTransactionManager(cf);
    }

    // Imperative equivalent (for contrast): state in ThreadLocal
    @Bean
    PlatformTransactionManager blockingTx(DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
}

go deeper

for a junior

Know there are two managers and reactive one returns Mono, not blocks.

for a middle

Explain state-in-Context vs ThreadLocal and that @Transactional chooses by return type.

for a senior

Discuss R2dbcTransactionManager wiring, why bridging blocking/reactive fails, void-return pitfall.

for a principal

Reason about mixed stacks, migration strategy, and where reactive tx boundaries belong in an event-loop architecture.

## The two abstractions Spring's transaction infrastructure has two top-level interfaces: - **`PlatformTransactionManager`** — the original, *imperative/blocking* abstraction. Implementations include `DataSourceTransactionManager` (plain JDBC), `JpaTransactionManager` (JPA/Hibernate), and `JtaTransactionManager`. Its methods (`getTransaction`, `commit`, `rollback`) run synchronously and *block* the calling thread until the database responds. - **`ReactiveTransactionManager`** — the *non-blocking* abstraction introduced in Spring 5.2 for reactive stacks (WebFlux + R2DBC). Its methods return `reactor.core.publisher.Mono`, so calling them schedules work without blocking the caller's thread. Both extend the marker interface `TransactionManager`. ## The ReactiveTransactionManager API ```java public interface ReactiveTransactionManager extends TransactionManager { Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition); Mono<Void> commit(ReactiveTransaction transaction); Mono<Void> rollback(ReactiveTransaction transaction); } ``` The returned `ReactiveTransaction` (default impl `GenericReactiveTransaction`) is the reactive analogue of `TransactionStatus`. ## The main implementation `org.springframework.r2dbc.connection.R2dbcTransactionManager` drives transactions over an R2DBC `ConnectionFactory` (the reactive equivalent of a JDBC `DataSource`). You construct it with a `ConnectionFactory`. Spring Data R2DBC / `DatabaseClient` participate in the transaction it starts. ## The critical conceptual difference: where state lives In the blocking world, transaction state (the current transaction, bound connection, synchronizations) is stored in **`ThreadLocal`** via `TransactionSynchronizationManager`. That works because a blocking call keeps the same thread for the whole transaction. In reactive code, a single logical request may execute across **many different threads** (operators can switch threads, `publishOn`/`subscribeOn`, event-loop hand-offs). A `ThreadLocal` would be lost the moment execution hops threads. So reactive transaction state is carried in the **Reactor Context** — an immutable key/value map that rides *with the subscription* through the operator chain, independent of which thread runs each step. ## @Transactional works with both You annotate methods the same way. Spring's `TransactionInterceptor` inspects the method's return type: if it returns a reactive type (`Mono`, `Flux`, any `Publisher`), it uses the reactive path and requires a `ReactiveTransactionManager` bean; otherwise it uses the imperative path with a `PlatformTransactionManager`. You cannot mix — a reactive method needs a reactive manager. ## When to use which - WebFlux + R2DBC / reactive Mongo → `ReactiveTransactionManager` (`R2dbcTransactionManager`). - Spring MVC + JDBC/JPA → `PlatformTransactionManager`. - Never wrap blocking JDBC in a reactive transaction expecting non-blocking behavior — JDBC blocks regardless. ## Gotcha There is no automatic bridging: a `@Transactional` method returning `void` but doing reactive work will be treated as *imperative* and the transaction will commit before the returned publisher runs. Always return the `Mono`/`Flux`.

  • Which bean type must exist for a @Transactional method returning Mono to work?
    A ReactiveTransactionManager bean (e.g. R2dbcTransactionManager). If only a PlatformTransactionManager is present, Spring cannot manage the reactive method reactively.
  • Why can't a reactive transaction use ThreadLocal like the blocking one does?
    Reactive execution hops between threads across operators/schedulers, so ThreadLocal state set on one thread is invisible on the next. State must ride with the subscription in the Reactor Context instead.

saying these in an interview costs you the question

  • Claiming @Transactional automatically makes blocking JDBC non-blocking
  • Saying reactive transaction state lives in ThreadLocal
  • Thinking one manager type serves both imperative and reactive methods
  • Returning void from a reactive @Transactional method

context

open as a page

How is transaction state propagated in reactive transactions, and why not ThreadLocal?

level: middleimportance: must knowfreq 50%

basics

~20 s

Reactive transaction state travels in the Reactor Context, an immutable map attached to the subscription. ThreadLocal can't be used because reactive operators run on different threads, so thread-bound state would be lost when execution hops threads.

open as a page

What is GenericReactiveTransaction and what role does it play in the ReactiveTransactionManager contract?

level: seniorimportance: should knowfreq 28%

basics

~10 s

GenericReactiveTransaction is the default implementation of the ReactiveTransaction handle returned by getReactiveTransaction. It represents one running reactive transaction (new-or-existing, rollback-only flag, completion state) and is passed back to commit or rollback.

open as a page

How do you apply a reactive transaction programmatically with TransactionalOperator versus declaratively with @Transactional?

level: seniorimportance: should knowfreq 42%

basics

~10 s

Declaratively, annotate a method returning Mono/Flux with @Transactional and Spring wraps it. Programmatically, build a TransactionalOperator from the ReactiveTransactionManager and apply it to your publisher with operator.transactional(...) or .as(operator::transactional).

open as a page

What goes wrong when reactive transactions are mixed with blocking JDBC/JPA, and how do you architect around it?

level: principalimportance: should knowfreq 30%

basics

~20 s

Blocking JDBC/JPA keeps transaction state in ThreadLocal and blocks threads; reactive transactions live in the Reactor Context on non-blocking drivers. They don't share a transaction, and blocking calls stall the event loop. Keep each stack whole: use R2DBC end-to-end for reactive transactions.

open as a page