← Back to list

Kotlin coroutines and R2DBC

Intro

Fyrkov · 2026-02-09 12:04 · 0 claps · 3.6 min read
#kotlin-coroutines #r2dbc #spring-boot #postgresql #reactive-programming
Open on Medium ↗
Wiki topics: 💻 · Programming 🌐 · Web Development 📱 · Mobile Development

Kotlin coroutines and R2DBC

Intro

This article demonstrates how to use reactive R2DBC drivers with Kotlin coroutines. To illustrate this, a typical outbox pattern implementation using Spring, Kotlin, PostgreSQL, and JDBC is reworked into a reactive form. Usage of Project Reactor is kept to a minimum to maintain a coroutines-first approach.

Why the hell do I need R2DBC?

  • non-blocking DB access so threads aren’t blocked on IO
  • higher concurrency with fewer threads
  • fits reactive stacks (WebFlux, reactive messaging)
  • useful for IO-heavy, high-load services

Why the hell do I need coroutines?

  • Kotlin first-class feature with language-level support
  • write async code in simple sequential style
  • less cumbersome than Reactor’s Flux/Mono chains

Wait, can I refrain from Reactor at all?

Other Spring reactive components used for end-to-end reactive applications, such as WebFlux or reactive Kafka client, are built on top of Reactor. However, this does not mean that one must use Reactor in the application code. It is possible to write code with coroutines only (no Flux/Mono in app), and keep Reactor at the edges as an underlying runtime. In general, there is nothing wrong with mixing Reactor and coroutines or staying with coroutines only.

Preparation of components

Now Let’s look at the components involved.

R2DBC

R2DBC drivers are database drivers that implement reactive, non-blocking access to relational databases. In simple terms, they are the reactive alternative to JDBC drivers. They have been around since about 2019 and have been steadily gaining popularity since then. However, JDBC remains dominant in most applications because of its maturity, stability and simplicity. This demo also aims to show that R2DBC usage can be straightforward in practice.

We will use the PostgreSQL driver for this demo:

implementation("org.postgresql:r2dbc-postgresql")

Spring

Spring provides the basic reactive tools to work with R2DBC drivers via the

implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")

It also transitively includes the Project Reactor library, which provides the reactive types and runtime.

JOOQ

In this demo we jOOQ to interact with the database. jOOQ supports R2DBC drivers since v3.15

The spring-boot-starter-jooq starter is not compatible because it uses JDBC. So we have to add

implementation("org.jooq:jooq")
implementation("org.jooq:jooq-kotlin-coroutines")

A DSL Context object has to be configured and provided like

@Bean
fun dslContext(cf: io.r2dbc.spi.ConnectionFactory): org.jooq.DSLContext =
    DSL.using(cf, SQLDialect.POSTGRES)

Flyway

Flyway does not support R2DBC drivers and therefore has to be configured separately to have its own separate jdbc connection from configs like:

spring:
  flyway:
    url: jdbc:postgresql://...

This is fine because in Spring Boot, the Flyway datasource is separate from the application datasource by default. The Flyway datasource is used only to run migrations. The Flyway datasource does not interfere with the R2DBC setup.

Testcontainers

Testcontainers have to be configured to work with R2DBC:

testImplementation("org.testcontainers:testcontainers-r2dbc")

How do we adapt the code?

With the setup in place, let’s look at what changes in the code.

In reactive mode, a jOOQ Query returns org.reactivestreams.Publisher<Record>. Publisher is defined by Reactive Streams, a standalone, technology-agnostic spec for asynchronous stream processing. Project Reactor is just a popular Java implementation of this spec.

Kotlin coroutines use a different model with suspending functions and Flow type, which are conceptually similar to Mono and Flux types from Project Reactor. Jetbrains also provides a bridge library kotlinx-coroutines-reactive that enables interoperability between Publisher (for example, a Flux from a jOOQ query) and coroutines.

A normal insert query

fun insert(aggregateType: String, aggregateId: String, payload: String): Long {
    return dsl.insertInto(table("outbox"))
        ...
        .fetchSingle()

becomes a suspending function

suspend fun insert(aggregateType: String, aggregateId: String, payload: String): Long {
    return dsl.insertInto(table)
        ...
        .awaitFirst()

A select query which returns a list of records

fun selectUnpublished(limit: Int): List<OutboxRecord> {
    return dsl.selectFrom(table)
        .where(field("published_at").isNull())
        .orderBy(field("id"))
        .limit(limit)
        .fetch { deser(it) }
}

becomes either a suspending function

suspend fun selectUnpublished(limit: Int): List<OutboxRecord> {
    val query = dsl.selectFrom(table("outbox"))
        .where(field("published_at").isNull())
        .orderBy(field("id"))
        .limit(limit)
    return Flux.from(query)
        .map { deser(it) }
        .collectList()
        .awaitSingle()
}

or a function that returns a Flow

fun selectUnpublishedAsFlow(limit: Int): Flow<OutboxRecord> {
    val query = dsl.selectFrom(table("outbox"))
        .where(field("published_at").isNull())
        .orderBy(field("id"))
        .limit(limit)
    return Flux.from(query)
        .asFlow()
        .map { deser(it) }
}

Additional notes

Transaction management

Since Spring 5.3, the Spring @Transactional is aware of Kotlin coroutines. When a suspend function is marked @Transactional, Spring correctly manages the transaction context within the CoroutineContext. NB: @Transactional in tests is looking still for JDBC Data source and does not work correctly if it is not configured.

Scheduling

Starting with Spring 6.1, @Scheduled officially supports Kotlin suspend functions.

Note on Java virtual threads

Java virtual threads first appeared as a preview in Java 19 (Project Loom) and became stable in Java 21. They also make blocking code scale much better by making threads lightweight and inexpensive. In fact, virtual threads reduce the need for reactive style for scalability. However, they are still blocking from the driver perspective (JDBC blocks sockets).

In Spring Boot, virtual threads are disabled by default, but they can be enabled by setting spring.threads.virtual.enabled=true, allowing the framework to use them.

Summary

In this demo, we transformed a synchronous JDBC app into a reactive R2DBC one with relatively few changes and little effort.

Further steps may include moving the remaining blocking parts to non-blocking alternatives and building a fully end-to-end reactive application.

Links


메타데이터
post_id
db0d34cf2d3f
slug
kotlin-coroutines-and-r2dbc-db0d34cf2d3f
url
https://medium.com/@fyrkov86/kotlin-coroutines-and-r2dbc-db0d34cf2d3f
canonical_url
https://medium.com/@fyrkov86/kotlin-coroutines-and-r2dbc-db0d34cf2d3f
author_url
https://medium.com/@fyrkov86
status
ok
fetched_at
2026-07-13 06:23:13