skip to content

R2DBC Reactive Repositories

R2DBC repositories return Mono and Flux over a non-blocking driver, with R2dbcEntityTemplate and DatabaseClient underneath. This is the piece that makes an end-to-end reactive stack possible, so it follows any WebFlux question.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is an R2dbcRepository, and what do its query methods return?

level: juniorimportance: must knowfreq 72%

answer

  1. Extends ReactiveCrudRepository
  2. Mono = 0/1, Flux = 0..N
  3. Nothing runs until subscribe
  4. Non-blocking driver, no .block()
  5. No persistence context / lazy loading

basics

~10 s

R2dbcRepository is Spring Data's reactive repository for relational databases. Its methods return reactive types: Mono<T> for zero-or-one result and Flux<T> for many, instead of blocking values or List.

solid answer

~40 s

R2dbcRepository<T, ID> is the reactive equivalent of JpaRepository. It extends ReactiveCrudRepository (and ReactiveSortingRepository), so CRUD and derived query methods return Project Reactor publishers: Mono<T> for a single/optional result, Flux<T> for many, Mono<Void> for fire-and-forget, and Mono<Long> for counts. Nothing runs until something subscribes; in a WebFlux controller you return the publisher and the framework subscribes. It runs on a non-blocking R2DBC driver, so there is no JDBC thread-per-request blocking. You declare an interface extending R2dbcRepository and Spring generates the implementation. Derived queries (findByEmail) and @Query native SQL both work, but there is no JPA persistence context, no lazy loading, and no relationship mapping.

code

java · 15 lines
java
public interface UserRepository extends R2dbcRepository<User, Long> {

    Flux<User> findByActiveTrue();

    Mono<User> findByEmail(String email);

    @Query("SELECT * FROM users WHERE created_at > :since")
    Flux<User> createdAfter(Instant since);
}

// In a WebFlux controller you just return the publisher:
@GetMapping("/users/{id}")
public Mono<User> byId(@PathVariable Long id) {
    return userRepository.findById(id); // framework subscribes
}

go deeper

for a junior

Know Mono vs Flux and that R2dbcRepository is the reactive repository.

for a middle

Understand laziness/subscription and that there is no persistence context or lazy loading.

for a senior

Can contrast with JPA feature-by-feature and reason about when R2DBC is the right choice.

for a principal

Sets the policy: reactive end-to-end or not at all; weighs operational cost of losing ORM features against non-blocking throughput.

**R2DBC** (Reactive Relational Database Connectivity) is a specification for accessing relational databases with a fully non-blocking, back-pressure-aware driver — the reactive counterpart to JDBC. Spring Data R2DBC builds repositories on top of it. **The repository hierarchy.** You declare an interface such as `interface UserRepository extends R2dbcRepository<User, Long>`. `R2dbcRepository<T, ID>` extends `ReactiveCrudRepository<T, ID>` and `ReactiveSortingRepository<T, ID>`. Spring generates the implementation at startup. Because everything is reactive, method return types are **Project Reactor** publishers, never blocking values or `List`: - `Mono<T>` — a stream of 0 or 1 element (`findById`, `save`). - `Flux<T>` — a stream of 0..N elements (`findAll`, `findByStatus`). - `Mono<Void>` — completion signal only (`deleteById`). - `Mono<Long>` — a scalar such as `count()`. **Laziness.** A `Mono`/`Flux` is a description of work, not the work itself. No SQL is sent until something **subscribes**. In Spring WebFlux you return the publisher from the controller and the framework subscribes for you; if you call a repository and never subscribe (and never return it), nothing happens. Never call `.block()` on the event-loop thread — that defeats the purpose and can deadlock the small event-loop thread pool. **What backs it.** A non-blocking driver such as `r2dbc-postgresql`, `r2dbc-h2`, or `r2dbc-mysql`, usually behind `r2dbc-pool` for connection pooling. Configuration is via `spring.r2dbc.url`/`username`/`password`, and Spring Boot auto-configures a `ConnectionFactory`. **What you get.** Derived query methods (`findByEmailAndActiveTrue`), paging via `Pageable` (returning `Flux` + a separate `count`), sorting, and custom SQL via `@Query("SELECT ... WHERE email = :email")` (native SQL only — there is no JPQL in R2DBC). **What you do NOT get (vs JPA).** No persistence context / first-level cache, no dirty checking (you must call `save` explicitly), no lazy loading, and no automatic relationship mapping (`@OneToMany`/`@ManyToOne` do not exist here). Joins and aggregates are composed by hand or written as SQL. This makes R2DBC simpler and more predictable but shifts more mapping work to you. **When to use it.** Choose R2DBC only when your whole request path is reactive (WebFlux) and you need non-blocking DB access for high concurrency with few threads. If any layer blocks, or you rely on rich ORM features, classic JPA/JDBC is usually the better fit.

  • Why must you avoid calling .block() inside a WebFlux request?
    WebFlux runs on a small event-loop thread pool. Blocking a loop thread stalls all requests it serves and can deadlock if it waits on work scheduled to the same pool. It reintroduces the thread-per-request cost R2DBC exists to avoid.
  • If a repository method returns Flux<User> but no one subscribes, what happens?
    Nothing. The Flux is only a description of the query; without a subscriber no SQL is sent to the database. Returning it from a controller is what triggers the framework to subscribe.

context

open as a page

What is R2dbcEntityTemplate and when would you use it instead of a repository?

level: middleimportance: should knowfreq 48%

basics

~10 s

R2dbcEntityTemplate is a mid-level helper that maps entities to/from rows using a fluent, type-safe API (select/insert/update/delete with Query and Criteria). Use it for dynamic queries a derived repository method cannot express.

open as a page

How does the DatabaseClient fluent API work for executing SQL in R2DBC?

level: seniorimportance: should knowfreq 45%

basics

~10 s

DatabaseClient is the low-level fluent API: databaseClient.sql("...").bind("name", value).map(row -> ...).all() (or .one()/.first()/.rowsUpdated()). It runs raw SQL with named parameter binding and manual row mapping, returning Mono/Flux.

open as a page

How does save() decide between INSERT and UPDATE in Spring Data R2DBC, and what breaks with assigned IDs?

level: seniorimportance: should knowfreq 38%

basics

~20 s

save() looks at the entity's @Id: if it is null (or 0 for primitives) the entity is treated as new and INSERTed; otherwise it is UPDATEd. With a manually assigned id, save() wrongly does an UPDATE that affects zero rows.

open as a page

How do transactions work with reactive R2DBC repositories, and why can't they use thread-local state?

level: principalimportance: should knowfreq 30%

basics

~10 s

Reactive transactions are managed by R2dbcTransactionManager and bound to the Reactor subscriber context, not to a thread. Use @Transactional on a method returning Mono/Flux, or TransactionalOperator, so the commit/rollback follows the reactive chain.

open as a page