ReactiveTransactionManager carries transaction state in Reactor context, so work outside the subscribed chain is outside that transaction.
Spring reactive transaction: subscription owns the context, not a thread
Build one chain
A reactive debit should insert its ledger row and update the account in one publisher chain, then apply a TransactionalOperator to that chain. The operator does not cover a second publisher subscribed manually inside doOnNext. That detached subscription can run after the first has failed or been cancelled. Subscription timing is part of the transaction contract.
Handle cancellation as a business case
A client disconnect can cancel a response publisher. If the transaction's completion depends on that publisher, cancellation can change when the database work is rolled back. Do not tie an essential payment or stock commitment to a response stream without deciding what cancellation means. A durable command record can separate acceptance from the client's connection; the debit lesson covers the normal rollback path.
Check the boundary under one subscription
Use a real R2DBC connection factory and subscribe once to the returned publisher. Inject failure after the first write; verify both writes are absent. Then cancel before completion and check database state. This is an architecture sketch; cancellation behavior must be verified against the chosen driver and transaction manager.
Implementation sketch
Mono<Void> transfer = balances.debit(accountId, 47)
.then(ledger.record(accountId, 47))
.then();
return transactionOperator.transactional(transfer);Cost and verification
One reactive transaction occupies a database connection until terminal completion. Slow upstream work and never-ending publishers can keep that connection unavailable to the pool.
Common Mistakes
- Do not call subscribe inside a transactional chain for a required write.
- Do not use a thread-local transaction assumption in Reactor code.
- Do not ignore cancellation when a client disconnects before completion.
Read next
Spring R2DBC reactive transaction: roll back the event marker with a rejected debit, Spring R2DBC subscription boundary: constructing SQL does not execute it, Spring R2DBC fixture isolation: keep incompatible H2 versions out of one test classpath, Spring imperative transaction: a worker thread does not inherit it.
