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 core — ResultSet.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?