spring-r2dbc

v2026.09.24

Spring Data R2DBC for reactive database access in Spring Boot 3.x. Covers R2dbcRepository, DatabaseClient, reactive transactions, and WebFlux integration. USE WHEN: user mentions "r2dbc", "reactive database", "R2dbcRepository", "DatabaseClient", "reactive SQL", "WebFlux database", "non-blocking database" DO NOT USE FOR: blocking JDBC - use `spring-data-jdbc` or `spring-data-jpa` instead, MongoDB reactive - use `spring-data-mongodb` with reactive repository

GitHub
Install command
npx skhub add claude-dev-suite/spring-r2dbc
Markdown
SKILL.md

Spring Data R2DBC

Quick Start

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-r2dbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>r2dbc-postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
# application.yml
spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/mydb
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    pool:
      enabled: true
      initial-size: 5
      max-size: 20

Entity Definition

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

@Table("products")
public class Product {

    @Id
    private Long id;

    @Column("product_name")
    private String name;

    private String description;
    private BigDecimal price;

    @Column("category_id")
    private Long categoryId;

    @CreatedDate
    private Instant createdAt;

    @LastModifiedDate
    private Instant updatedAt;

    private boolean active;

    // Constructors, getters, setters...
}
// For immutable entities with records (Java 17+)
@Table("orders")
public record Order(
    @Id Long id,
    @Column("customer_id") Long customerId,
    BigDecimal total,
    OrderStatus status,
    @CreatedDate Instant createdAt
) {
    public Order withStatus(OrderStatus newStatus) {
        return new Order(id, customerId, total, newStatus, createdAt);
    }
}

Repository Interface

import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.Query;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface ProductRepository extends R2dbcRepository<Product, Long> {

    // Automatic derived queries
    Flux<Product> findByActiveTrue();
    Flux<Product> findByNameContainingIgnoreCase(String name);
    Flux<Product> findByCategoryId(Long categoryId);
    Flux<Product> findByPriceBetween(BigDecimal min, BigDecimal max);
    Mono<Product> findByNameIgnoreCase(String name);

    // Ordering and limiting
    Flux<Product> findTop10ByActiveTrueOrderByCreatedAtDesc();

    // Count and Exists
    Mono<Long> countByActiveTrue();
    Mono<Boolean> existsByName(String name);

    // Custom query
    @Query("SELECT * FROM products WHERE category_id = :categoryId AND price < :maxPrice")
    Flux<Product> findByCategoryWithMaxPrice(Long categoryId, BigDecimal maxPrice);

    @Query("UPDATE products SET active = false WHERE id = :id")
    @Modifying
    Mono<Integer> deactivateProduct(Long id);

    // Projection with DTO
    @Query("SELECT id, product_name as name, price FROM products WHERE active = true")
    Flux<ProductSummary> findAllSummaries();
}

public record ProductSummary(Long id, String name, BigDecimal price) {}

Full Reference: See database-client.md for complex queries with DatabaseClient.


Service Layer

@Service
@RequiredArgsConstructor
@Slf4j
public class ProductService {

    private final ProductRepository productRepository;

    // Create
    public Mono<Product> createProduct(CreateProductRequest request) {
        Product product = Product.create(request.name(), request.description(), request.price());
        product.setCategoryId(request.categoryId());
        return productRepository.save(product)
            .doOnSuccess(p -> log.info("Created product: {}", p.getId()));
    }

    // Read
    public Mono<Product> getProduct(Long id) {
        return productRepository.findById(id)
            .switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
    }

    public Flux<Product> getAllActiveProducts() {
        return productRepository.findByActiveTrue();
    }

    // Update
    public Mono<Product> updateProduct(Long id, UpdateProductRequest request) {
        return productRepository.findById(id)
            .switchIfEmpty(Mono.error(new ProductNotFoundException(id)))
            .map(product -> {
                if (request.name() != null) product.setName(request.name());
                if (request.price() != null) product.setPrice(request.price());
                return product;
            })
            .flatMap(productRepository::save);
    }

    // Delete (soft delete)
    public Mono<Void> deactivateProduct(Long id) {
        return productRepository.deactivateProduct(id)
            .filter(count -> count > 0)
            .switchIfEmpty(Mono.error(new ProductNotFoundException(id)))
            .then();
    }
}

Reactive Transactions

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final OrderItemRepository orderItemRepository;

    @Transactional
    public Mono<Order> createOrder(CreateOrderRequest request) {
        return validateProducts(request.items())
            .then(calculateTotal(request.items()))
            .flatMap(total -> {
                Order order = new Order(null, request.customerId(), total, OrderStatus.PENDING, null);
                return orderRepository.save(order);
            })
            .flatMap(order -> saveOrderItems(order.id(), request.items())
                .then(Mono.just(order)));
    }

    private Mono<BigDecimal> calculateTotal(List<OrderItemRequest> items) {
        return Flux.fromIterable(items)
            .flatMap(item -> productRepository.findById(item.productId())
                .map(p -> p.getPrice().multiply(BigDecimal.valueOf(item.quantity()))))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

Full Reference: See transactions.md for TransactionalOperator and relation handling.


Best Practices

DoDon't
Use R2DBC for WebFlux applicationsMix JDBC and R2DBC
Configure connection poolUse without pool
Handle relations manually with batch queriesExpect JPA-like lazy loading
Use @Transactional or TransactionalOperatorForget transaction management
Use StepVerifier for testingUse .block() in production

When NOT to Use This Skill

  • Blocking applications - Use spring-data-jdbc or spring-data-jpa
  • Complex ORM features - R2DBC is simple, use JPA for lazy loading
  • Not using WebFlux - R2DBC is for reactive stack
  • MongoDB reactive - Use spring-data-mongodb reactive repositories

Common Pitfalls

ErrorCauseSolution
NoSuchBeanDefinitionException: ConnectionFactoryMissing R2DBC driverAdd r2dbc-postgresql/mysql dependency
Connection timeoutPool exhaustedIncrease max-size, check connection leaks
TransactionRequiredExceptionMissing @TransactionalAdd annotation or use TransactionalOperator
Entity not mappedMissing annotationsVerify @Table, @Id, @Column
N+1 queriesLoading relationsUse batch queries with IN clause

Anti-Patterns

Anti-PatternProblemSolution
Using .block() in productionBlocks event loopUse reactive operators
N+1 queries for relationsPerformance issuesUse batch queries with IN
Missing connection poolConnection exhaustionConfigure r2dbc-pool
Large transactionsConnection held too longKeep transactions short
No error handlingSilent failuresUse onErrorResume, onErrorMap

Quick Troubleshooting

ProblemDiagnosticFix
Connection timeoutCheck pool settingsIncrease max-size, check leaks
Entity not mappedCheck annotationsAdd @Table, @Id, @Column
Transaction not workingCheck @TransactionalUse TransactionalOperator if needed
Query returns emptyCheck column namesVerify mapping matches DB
Pool exhaustedMonitor active connectionsIncrease pool, fix leaks

Reference Files

FileContent
database-client.mdDatabaseClient, Dynamic Queries, Aggregations
transactions.mdTransactions, Relations, Pagination
advanced.mdAuditing, Converters, Pool Config, Testing

External Documentation

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/databases/spring-r2dbc

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1