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 JPA | Spring Data R2DBC | |
|---|---|---|
| Underlying API | JDBC (blocking) | R2DBC (non-blocking) |
| Returns | entities, List, Optional | Mono, Flux |
| ORM features | rich (lazy loading, cascades, dirty checking) | minimal — no lazy loading, no relations magic |
| Stack | Spring MVC | Spring 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:
| Database | Driver artifact |
|---|---|
| PostgreSQL | org.postgresql:r2dbc-postgresql |
| MySQL | io.asyncer:r2dbc-mysql |
| MariaDB | org.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@Querywith 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.
Related Topics
- Spring Data JPA — the blocking, full-ORM counterpart.
- Spring WebFlux — the reactive web stack R2DBC pairs with.
- Mono & Flux — the return types of every repository method.
- Reactive REST APIs — wiring repositories into endpoints.
- WebFlux vs Spring MVC — choosing between the stacks.
- PostgreSQL Integration — the blocking JDBC setup.