Skip to content
Spring Boot sb reactive 3 min read

R2DBC (Reactive SQL)

R2DBC (Reactive Relational Database Connectivity) is a non-blocking SQL API — the reactive answer to JDBC. With Spring Data R2DBC your repositories return Mono and Flux, so database access fits naturally into a WebFlux pipeline without ever blocking a Netty event loop thread. This page covers the starter, repositories, the template API, drivers, and configuration.

Why JPA/Hibernate cannot be reactive

This is the single most important thing to understand. JDBC is a blocking API at its coreResultSet.next() blocks the calling thread until the database responds. JPA and Hibernate are built on top of JDBC and inherit that blocking behaviour, plus they rely on a thread-bound persistence context and lazy loading that assume a synchronous call stack.

You therefore cannot use Spring Data JPA in a reactive application. Wrapping JPA calls in Schedulers.boundedElastic() only hides them on another thread pool; it does not make them non-blocking and you lose the scalability benefit. For true non-blocking SQL you need a different protocol implementation from the ground up — that is R2DBC.

Spring Data JPASpring Data R2DBC
Underlying APIJDBC (blocking)R2DBC (non-blocking)
Returnsentities, List, OptionalMono, Flux
ORM featuresrich (lazy loading, cascades, dirty checking)minimal — no lazy loading, no relations magic
StackSpring MVCSpring WebFlux

Note: R2DBC is not an ORM. There is no lazy loading and no automatic relationship management — you fetch related rows with explicit queries. This keeps it simple and predictable.

Adding the starter and a driver

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- a reactive driver for your database -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>r2dbc-postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

Driver coordinates for the common databases:

DatabaseDriver artifact
PostgreSQLorg.postgresql:r2dbc-postgresql
MySQLio.asyncer:r2dbc-mysql
MariaDBorg.mariadb:r2dbc-mariadb
H2 (testing)io.r2dbc:r2dbc-h2

Configuration

R2DBC uses an r2dbc: URL scheme, not jdbc:.

spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/shop
    username: app
    password: secret
    pool:
      initial-size: 5
      max-size: 20

Warning: R2DBC has no Flyway/Liquibase reactive integration for runtime migrations on the same connection factory. Run schema migrations separately (e.g. a JDBC-based Flyway step at startup), or initialize the schema with spring.sql.init.schema-locations.

Entities

Entities are lightweight POJOs (records work well) annotated with Spring Data Relational annotations — note these are org.springframework.data.*, not jakarta.persistence.*.

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.core.mapping.Column;

@Table("products")
public record Product(
        @Id Long id,
        @Column("name") String name,
        BigDecimal price) { }

ReactiveCrudRepository

Extend ReactiveCrudRepository (or R2dbcRepository) to get reactive CRUD methods for free. Every method returns Mono or Flux.

public interface ProductRepository extends ReactiveCrudRepository<Product, Long> {

    Flux<Product> findByPriceLessThan(BigDecimal max);   // derived query

    @Query("SELECT * FROM products WHERE name ILIKE :term")
    Flux<Product> search(String term);                   // explicit SQL
}

Using it in a service is just operator composition:

@Service
class ProductService {

    private final ProductRepository repo;

    ProductService(ProductRepository repo) {
        this.repo = repo;
    }

    Flux<Product> cheaperThan(BigDecimal max) {
        return repo.findByPriceLessThan(max);
    }

    Mono<Product> create(Product p) {
        return repo.save(p);            // INSERT, returns the saved row reactively
    }

    Mono<Void> delete(Long id) {
        return repo.deleteById(id);
    }
}

Note: Derived query keywords (findBy, LessThan, Containing, etc.) work like in JPA, but R2DBC supports a smaller subset and no automatic joins. Reach for @Query with hand-written SQL when needed.

R2dbcEntityTemplate

For programmatic, fluent queries (the reactive counterpart to JdbcTemplate), inject R2dbcEntityTemplate.

@Service
class ProductQueryService {

    private final R2dbcEntityTemplate template;

    ProductQueryService(R2dbcEntityTemplate template) {
        this.template = template;
    }

    Flux<Product> inRange(BigDecimal min, BigDecimal max) {
        return template.select(Product.class)
                .matching(Query.query(
                        Criteria.where("price").between(min, max)))
                .all();
    }

    Mono<Product> insert(Product p) {
        return template.insert(Product.class).using(p);
    }
}

Transactions

Reactive transactions are driven by TransactionalOperator or the familiar @Transactional annotation (backed by a R2dbcTransactionManager, which Spring Boot auto-configures).

@Transactional
Mono<Order> placeOrder(Order order) {
    return orderRepo.save(order)
            .flatMap(saved -> inventoryRepo.decrement(saved.productId())
                    .thenReturn(saved));
}

The transaction commits when the returned publisher completes successfully and rolls back if it errors.

Last updated June 13, 2026
Was this helpful?