With operator.transactional(mono/flux), exactly which reactive signal triggers a commit and which triggers a rollback? What happens on cancellation?
answer
- onComplete → commit
- onError → rollback + rethrow
- cancel → rollback
- empty-but-complete still commits
- onErrorResume inside = swallows = commit
basics
~10 sIt 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.
solid answer
~40 sThe commit/rollback decision is driven by the terminal signal of the wrapped `Mono`/`Flux`. A normal completion (`onComplete`, after all `onNext` for a Flux) triggers a **commit**; an **error signal** (`onError`) triggers a **rollback** and then the same error is propagated to the caller. **Cancellation** — the downstream unsubscribing before the publisher finishes (e.g. a `take(n)` upstream, a timeout, or the client disconnecting) — also triggers a **rollback**, because the unit of work never completed. A subtle point: for a `Flux`, a commit only happens after the stream fully completes, so a long or infinite Flux keeps the transaction open the whole time. Because the boundary is the reactive signal, the transaction is only committed when the framework actually subscribes and drains the returned publisher — an unsubscribed pipeline does nothing.
code
java · 10 linesMono<Order> place = orderRepo.save(order)
.flatMap(saved -> inventoryRepo.decrement(saved));
// Placement A: recovery is DOWNSTREAM of the tx -> error rolls back, then we map it
Mono<Order> a = rxtx.transactional(place)
.onErrorResume(ex -> Mono.error(new OrderFailed(ex))); // rollback happened
// Placement B: recovery is INSIDE the tx -> error is swallowed -> COMMIT
Mono<Order> b = rxtx.transactional(
place.onErrorResume(ex -> Mono.just(fallbackOrder))); // commits fallback!go deeper
Know completion commits and error rolls back.
Add cancellation → rollback and that a Flux commits only after full completion.
Reason about operator placement (recovery inside vs outside the wrap) changing commit/rollback.
Discuss long-open-transaction risk with large/unbounded Flux and connection-pool/lock implications.
## The signal-to-outcome mapping `TransactionalOperator` binds the transaction lifecycle to the **Reactive Streams terminal signals** of the publisher you wrap: | Signal | Outcome | |--------|---------| | `onComplete` (Mono/Flux finishes normally) | **commit** | | `onError(Throwable)` | **rollback**, then the error is re-emitted downstream | | `cancel` (downstream unsubscribes early) | **rollback** | There is no separate 'success value' concept — an empty `Mono` that completes without emitting still **commits**, because the terminal signal is `onComplete`. ## Why cancellation rolls back Cancellation means the subscriber said 'stop, I don't want more.' Common causes: an upstream `take(1)`, a `timeout(...)` firing, the HTTP client disconnecting, or `Flux`/`Mono` being discarded. The unit of work did not reach its defined end, so committing partial work would be unsafe — the operator rolls back. This is a real production gotcha: if you put `.transactional()` **below** an operator that cancels the source (e.g. `.next()` on a Flux, or `.take(1)`), you can get surprise rollbacks. ## Flux commits only at the end For a `Flux`, the commit is deferred until the **whole stream completes**. Every `onNext` runs inside the open transaction. Consequences: - Wrapping a very large or unbounded `Flux` holds the DB transaction (and its connection) open for the entire duration — long-running transactions, lock contention, connection-pool pressure. - If element #500 errors, everything already emitted is rolled back (it was one atomic transaction). ## Nothing runs until subscription `transactional()` returns a new publisher; the transaction begins only when someone **subscribes** to it. If you forget to return it, or call it for its 'side effect' without subscribing, no transaction ever opens. Always compose it into the chain you hand back to WebFlux (which subscribes for you). ## Interplay with error-handling operators Operators like `onErrorResume`/`onErrorReturn` **swallow** the error and turn it into a normal completion. If you place them **inside** the wrapped publisher (upstream of `.transactional`), the operator never sees an `onError` — so it **commits**. Placement matters: - `rxtx.transactional(work.onErrorResume(...))` → error handled inside → likely commit. - `rxtx.transactional(work).onErrorResume(...)` → transaction sees the error → rollback, then you recover downstream. ## Forcing rollback without an error If you need to roll back a technically-successful flow, use the callback form `execute(tx -> ...)` and call `tx.setRollbackOnly()` — covered in the programmatic-callback question.
- If a Flux emits 3 items then errors on the 4th, what is persisted?Nothing from that transaction — it is one atomic unit; the onError triggers a rollback of all work done during the stream, including the first three elements.
- Does an empty Mono (completes without a value) commit or roll back?It commits. The terminal signal is onComplete; emitting a value is not required for commit.
saying these in an interview costs you the question
- Believing each onNext of a Flux commits independently
- Assuming cancellation commits the partial work
- Not realizing onErrorResume placed inside the wrapped publisher causes a commit