Working with Reactive Data Access

1. Configuring R2DBC Data Source

Property Description
spring.r2dbc.url r2dbc:postgresql://host/db
spring.r2dbc.username/password Credentials
spring.r2dbc.pool.initial-size/max-size Pool tuning
spring.r2dbc.pool.max-idle-time Idle eviction
spring.sql.init.mode Init scripts: always/never

2. Creating Reactive Entities (@Table)

Example: R2DBC entity as record

@Table("users")
public record User(@Id Long id, String email, Instant createdAt) {}
Note: R2DBC is NOT JPA — no relationships, lazy loading, or dirty tracking. Use plain mappings.

3. Using ReactiveCrudRepository Interface

Example: Reactive repository with Mono and Flux

public interface UserRepo extends R2dbcRepository<User, Long> {
  Mono<User> findByEmail(String email);
  Flux<User> findByCreatedAtAfter(Instant since);
}

4. Writing Custom Reactive Queries (@Query)

Example: Reactive custom JPQL select and update

@Query("SELECT * FROM orders WHERE customer_id = :cid AND total > :min")
Flux<Order> highValue(@Param("cid") Long cid, @Param("min") BigDecimal min);

@Modifying
@Query("UPDATE orders SET status='CANCELLED' WHERE id = :id")
Mono<Integer> cancel(@Param("id") Long id);

5. Implementing Pagination (Pageable)

Example: Reactive pagination

Flux<User> page(Pageable p);
Mono<Page<User>> pageOf(Pageable p) { /* combine count + slice */ }
Element Behavior
Pageable as parameter Skip + limit applied
Page<T> Use PageableExecutionUtils.getPage(...)

6. Using Database Client (DatabaseClient)

Example: Raw SQL query with DatabaseClient

Mono<User> user = client.sql("SELECT id, email FROM users WHERE id = :id")
  .bind("id", 42L)
  .map((row, meta) -> new User(row.get("id", Long.class), row.get("email", String.class), null))
  .one();

7. Handling Transactions (TransactionalOperator)

Example: Reactive transactional operator

@Service
public class TransferService {
  private final TransactionalOperator tx;
  public Mono<Void> transfer(Long from, Long to, BigDecimal amt) {
    return debit(from, amt).then(credit(to, amt)).as(tx::transactional).then();
  }
}
// or
@Transactional public Mono<Void> method() { /* … */ }

8. Implementing Reactive Specifications

Approach Detail
Criteria API org.springframework.data.relational.core.query.Criteria
R2dbcEntityTemplate template.select(User.class).matching(Query.query(where("email").is(x)))
Specifications Not supported (JPA-only feature)

9. Using Reactive Caching

Example: Reactive Redis cache-aside pattern

public Mono<User> byId(Long id) {
  return reactiveRedis.opsForValue().get("user:" + id)
    .switchIfEmpty(repo.findById(id)
      .flatMap(u -> reactiveRedis.opsForValue().set("user:" + id, u, Duration.ofMinutes(10)).thenReturn(u)));
}
Warning: Spring Cache annotations don't natively understand Mono/Flux — use CacheMono / manual caching.

10. Testing Reactive Repositories (StepVerifier)

Example: Test reactive repo with StepVerifier

@DataR2dbcTest
class UserRepoTests {
  @Autowired UserRepo repo;
  @Test void findsByEmail() {
    repo.save(new User(null, "a@b", Instant.now())).block();
    StepVerifier.create(repo.findByEmail("a@b"))
      .expectNextMatches(u -> u.email().equals("a@b"))
      .verifyComplete();
  }
}

11. Using R2DBC vs JDBC Comparison

JDBC + JPA

  • Blocking, mature ecosystem
  • Rich ORM (relations, lazy)
  • Connection-per-request
  • Best for traditional apps

R2DBC

  • Non-blocking I/O
  • Mapper-only, no relations
  • Higher concurrency at lower thread count
  • Best for high-fan-in services