skip to content

Programmatic Transactions

Managing transactions in code rather than by annotation: TransactionTemplate, driving the manager directly, working with TransactionStatus and savepoints, and the reactive TransactionalOperator. Interviewers ask when a scenario needs a boundary narrower or more dynamic than a method.

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

explore

questions

20

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

What is TransactionStatus in Spring's programmatic transaction API, and what does setRollbackOnly() do?

level: juniorimportance: must knowfreq 55%

basics

~10 s

TransactionStatus is a handle to the current transaction that PlatformTransactionManager gives you. Calling setRollbackOnly() marks the transaction so it can only roll back — commit will not succeed.

open as a page

What is Spring's TransactionTemplate and when would you reach for it instead of @Transactional?

level: juniorimportance: must knowfreq 55%

basics

~20 s

TransactionTemplate runs a block of code inside a database transaction programmatically. You call execute() and pass a callback; Spring starts a transaction, runs your code, then commits or rolls back. Use it when you need transaction control in code rather than the @Transactional annotation.

open as a page

Walk through the correct try/catch pattern for a manager-driven transaction. What goes wrong if the catch block is written incorrectly?

level: middleimportance: must knowfreq 45%

basics

~10 s

Get a TransactionStatus, do the work, and commit inside try. In catch, call rollback(status) and rethrow. If you forget rollback, a failed transaction stays open and the connection may leak or auto-commit dirty data.

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 rollback behavior with the direct PlatformTransactionManager differ from @Transactional's default rule-based rollback?

level: seniorimportance: must knowfreq 50%

basics

~20 s

@Transactional rolls back automatically only on unchecked exceptions (RuntimeException/Error) and commits on checked ones. With the direct manager there are no rules at all — you must call rollback() yourself for whatever cases you want to abort.

open as a page

What are the exact rollback rules for TransactionTemplate — which exceptions roll back, and how does setRollbackOnly() differ from throwing?

level: seniorimportance: must knowfreq 50%

basics

~10 s

If your callback throws a RuntimeException or Error, TransactionTemplate rolls back and rethrows. If it returns normally, it commits. You can also force rollback without throwing by calling status.setRollbackOnly() and returning normally.

open as a page

What is manager-driven programmatic transaction management, and how do you use PlatformTransactionManager directly?

level: juniorimportance: should knowfreq 35%

basics

~10 s

Instead of the @Transactional annotation, you inject PlatformTransactionManager, call getTransaction(...) to start a transaction, run your code, then call commit() on success or rollback() on failure yourself.

open as a page

What does TransactionStatus.isNewTransaction() tell you, and why does it matter with transaction propagation?

level: middleimportance: should knowfreq 40%

basics

~10 s

isNewTransaction() returns true if this call actually started a brand-new physical transaction, and false if it joined an already-running one. It matters because only the outermost, new transaction actually commits or rolls back.

open as a page

Explain TransactionTemplate.execute(TransactionCallback) versus executeWithoutResult, and what TransactionCallback / TransactionStatus give you.

level: middleimportance: should knowfreq 40%

basics

~20 s

execute() takes a TransactionCallback that returns a value, and hands it a TransactionStatus you can use (e.g. to mark rollback). executeWithoutResult() is a Spring 5.2 convenience for void work — it takes a Consumer<TransactionStatus> so you don't return null.

open as a page

Why does manager-driven transaction management avoid the proxy self-invocation problem, and what are the trade-offs versus @Transactional?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Because there is no AOP proxy involved — the transaction starts because your code calls getTransaction() directly, not because a proxy intercepted a call. So @Transactional's self-invocation gotcha (private/internal calls bypassing the proxy) simply doesn't exist. The cost is more boilerplate.

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 does TransactionStatus.isRollbackOnly() report, and how does it relate to UnexpectedRollbackException?

level: seniorimportance: should knowfreq 35%

basics

~10 s

isRollbackOnly() returns true if the transaction has been marked rollback-only (so it can no longer commit). If an inner method sets that flag and the outer tries to commit, Spring throws UnexpectedRollbackException.

open as a page

How do you use Spring's SavepointManager API (createSavepoint / rollbackToSavepoint / releaseSavepoint) for nested control inside a transaction?

level: seniorimportance: should knowfreq 30%

basics

~10 s

TransactionStatus implements SavepointManager. You call createSavepoint() to mark a point, do risky work, then rollbackToSavepoint(sp) to undo just that work (keeping earlier changes), or releaseSavepoint(sp) to discard the marker if things went fine.

open as a page

How do you tune propagation, isolation, timeout and read-only on a TransactionTemplate for a specific unit of work, and what's the correct way to have multiple different settings?

level: seniorimportance: should knowfreq 35%

basics

~20 s

TransactionTemplate is itself a TransactionDefinition, so you configure propagation, isolation, timeout, and read-only on the template with setters like setPropagationBehavior, setIsolationLevel, setTimeout, setReadOnly. For different settings, create separate template instances rather than mutating a shared one.

open as a page

When would you choose the raw PlatformTransactionManager over TransactionTemplate or @Transactional, and how do savepoints and per-unit commits fit in?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Use the raw manager only when the higher abstractions can't express the boundary: many independent commits in a loop, boundaries opened in one place and closed in another, or savepoints for partial rollback. Otherwise prefer @Transactional, then TransactionTemplate.

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

In a large batch that must keep good rows and skip bad ones, how would you design transaction handling using savepoints vs REQUIRES_NEW, and what trade-offs drive the choice?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Wrap each item so a failure only undoes that item. Either use PROPAGATION_NESTED (a savepoint per item inside one big transaction) or PROPAGATION_REQUIRES_NEW (a separate transaction per item). Savepoints share one connection; REQUIRES_NEW commits each item independently.

open as a page

Is TransactionTemplate thread-safe, and how does that shape how you construct, share, and reuse it — including choosing it over @Transactional at scale?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Yes — once fully configured, a TransactionTemplate is thread-safe and meant to be shared as a single instance (typically a Spring bean). The catch: never change its settings after publishing it, because the definition fields aren't safe to mutate concurrently.

open as a page