DatabaseClient returns a reactive publisher whose SQL work starts on subscription, so an unobserved publisher is an unperformed database operation.
Spring R2DBC subscription boundary: constructing SQL does not execute it
Keep the publisher connected to its caller
The fixture constructs an UPDATE publisher that would subtract seven units from SKU-47. Before subscribing, a separate query still sees 19 units. StepVerifier subscribes, observes one updated row, and the next query sees 12. Returning or composing that publisher is part of the command contract. Calling database.sql and discarding the resulting Mono does not schedule a write.
The test uses block only in setup and assertions. A reactive request handler should return the pipeline instead of blocking its event-loop thread. Blocking JDBC, filesystem or remote calls inside a reactive chain can pin threads and erase the capacity benefit. Demand and subscription covers publisher semantics at the web boundary; JdbcTemplate is deliberately blocking.
Choose the data-access model consciously
R2DBC is not a transparent performance switch for a JDBC repository. Driver behavior, connection pools, transaction context, supported SQL features and debugging all need their own checks. If the service already performs blocking work on request threads and throughput is adequate, rewriting every repository to return Mono can add operational cost without solving a measured bottleneck. The source kit keeps the R2DBC fixture separate from its existing JDBC project.
Checked excerpt
Mono<Long> debit = database.sql(
"update stock_balance set available = available - :units "
+ "where sku = :sku and available >= :units")
.bind("units", 7).bind("sku", "SKU-47")
.fetch().rowsUpdated();
assertEquals(19, available());
StepVerifier.create(debit).expectNext(1L).verifyComplete();
assertEquals(12, available());Verification boundary
The Java 21 / Spring Boot 4 source kit checks sqlPublisherDoesNotRunBeforeSubscription in the isolated reactive-r2dbc Maven project.
Cost and limits
The test shows deferred execution, not higher throughput. Each subscribed operation still needs a database connection and SQL execution; pool capacity and database scheduling remain limits.
Common Mistakes
- Do not construct a Mono and discard it while assuming the SQL ran.
- Do not call block on a reactive request thread.
- Do not assume switching APIs removes database contention.
Read next
Spring reactive foundations: request values and cancel a subscription, Spring JdbcTemplate: bound values and visible database constraints, Spring R2DBC named binds: keep a receipt identifier out of SQL syntax, Spring R2DBC reactive transaction: roll back the event marker with a rejected debit, Spring R2DBC fixture isolation: keep incompatible H2 versions out of one test classpath.
