Skip to content
Exposed docs1.11

JDBC vs R2DBC

Latest stable Based on Exposed release 1.11.0

Choose the driver and transaction model before choosing repository syntax. Both paths provide Exposed DSL and repository helpers, but they differ in blocking behavior, context propagation, connection ownership, framework integration, and failure testing.

ConcernJDBCR2DBC
Driver APIBlocking JDBC DataSource/ConnectionReactive Streams-based ConnectionFactory/R2DBC connection
Kotlin surfaceRegular functions; blocking calls need an appropriate threadsuspend functions and Flow
Transaction entryExposed transaction or Spring JDBC transaction integrationCaller/framework-owned suspendTransaction; Spring coroutine repository bridge can open it
ContextUsually thread-bound transaction contextCoroutine transaction context; do not detach work into unrelated scopes
CancellationThread interruption is not a universal statement-cancel contractCoroutine cancellation propagates as a signal, but driver/database cancellation timing varies
Connection ownerApplication/framework owns DataSource and poolApplication/framework owns ConnectionFactory and pool
Spring fitMVC, imperative services, Spring Batch, conventional transaction managersWebFlux/coroutine call chains and R2DBC-aware infrastructure
TestcontainersJDBC driver plus database containerMatching R2DBC driver plus database container; JDBC may still be needed by migration tools
Migration costExisting blocking libraries and transactions remain naturalCall chain, driver, pool, migrations, observability, and tests must become R2DBC-aware

Choose JDBC when the database driver and surrounding framework are blocking, the service already uses imperative Spring transactions, or the workload is dominated by batch/ETL code that expects JDBC semantics. Java virtual threads can reduce the cost of blocking thread-per-request code without changing the driver contract.

Choose R2DBC when the supported database has a suitable R2DBC driver and the complete request path—framework, repository, transaction, pool, and downstream work—stays coroutine/reactive. One R2DBC repository inside an otherwise blocking call chain does not create an end-to-end non-blocking system.

R2DBC changes how waiting work uses threads. It does not remove database latency, lock contention, query planning, network round trips, or pool limits. A JDBC path with a well-sized pool and virtual threads may outperform or simplify an R2DBC path for a given workload. Compare representative latency, throughput, connection occupancy, CPU, and failure recovery before migrating.

With JDBC, transaction context is commonly thread-bound. With R2DBC, the transaction belongs to the coroutine context opened by suspendTransaction. Returning a Flow and collecting it after that context ends is unsafe. In either path, the service layer should own the business transaction instead of letting every repository method start an independent transaction.

The 1.11.0 Spring JDBC module integrates Exposed repositories with imperative Spring data access. The Spring R2DBC module preserves suspend/Flow signatures and delegates execution to Exposed R2DBC transactions. Do not assume every Spring component is interchangeable: migration tools, batch libraries, and third-party integrations may still require JDBC even when runtime queries use R2DBC.

  1. Run the current JDBC workload and record latency, connection occupancy, timeout, and cancellation behavior.
  2. Verify R2DBC feature and SQL parity with the selected driver.
  3. Port one vertical slice including framework handler, transaction, repository, pool, and tests.
  4. Run H2 feedback tests and sequential Testcontainers tests with the production database.
  5. Exercise timeout, cancellation, pool exhaustion, database restart, and shutdown.
  6. Compare results. Keep JDBC when the R2DBC path adds complexity without a measured operational benefit.