WebFlux Migration for Reactive Microservice

A comprehensive guide documenting the migration of member-service from Servlet/JPA (blocking) to WebFlux/R2DBC (reactive).

Commit: 7ecb2e5 (Jan 2026)
Branch: feature/webflux-migration


Why Migrate to WebFlux?

The Problem with Blocking Architecture

Traditional Spring MVC with JPA uses blocking I/O:

Request → Thread waits for DB → Thread waits for response → Return

Each request holds a thread while waiting. Under high load, threads get exhausted.

The Reactive Solution

Spring WebFlux with R2DBC uses non-blocking I/O:

Request → Subscribe to DB → Thread released → Callback when ready → Return

Threads are released immediately, allowing massive concurrency with fewer resources.

Benefits

  • Better scalability — Handle more concurrent requests with fewer threads
  • Resource efficiency — No thread blocking during I/O operations
  • Backpressure support — Control data flow between producer and consumer
  • Modern architecture — Aligns with reactive microservice patterns

Stress Test Evidence — BEFORE Migration

Test Date: 2026-01-26
Stack: Servlet (Tomcat) + JPA (JDBC) + PostgreSQL
Endpoint: POST /auth/token (login)
Flow: auth-service → member-service → PostgreSQL

Results Summary

Concurrent Users Success Rate Throughput P50 Latency
1 (sequential) 100% 3.2 req/s 292 ms
2 95% 7.8 req/s 224 ms
5 60% 18.0 req/s 232 ms
10 32.5% 27.8 req/s 246 ms
20 17.5% 29.2 req/s 615 ms
50 20% 36.2 req/s 1259 ms

Key Findings

  1. Breaking point at ~5 concurrent users — Success rate drops from 95% to 60%
  2. 80% failure rate at high concurrency — System cannot handle 20+ users
  3. Latency explodes under load — P50 goes from 224ms (2 users) to 1259ms (50 users) = 5.6x increase
  4. Throughput plateaus at ~36 req/s — Adding more users doesn’t increase throughput

Visual: Success Rate Collapse

Success Rate
100% |*
 80% |
 60% |    *
 40% |        *
 20% |            *---*---*
  0% +-------------------------
     1   2   5  10  20  50   Concurrent Users

BEFORE vs AFTER Comparison (TBD)

Metric BEFORE (Servlet/JPA) AFTER (WebFlux/R2DBC) Change
Success Rate (5 users) 60% TBD
Success Rate (50 users) 20% TBD
P50 Latency (50 users) 1259 ms TBD
Max Throughput ~36 req/s TBD

Migration Overview — Change Impact by Layer

The migration follows a clear dependency order. Changes at one layer ripple to the next:

Model (Entity) → Repository → Service → Controller
     ↓              ↓            ↓           ↓
  Core changes   Interface   Handle what   Reactive
  (most work)    changes     JPA did      wrappers

Layer Summary

Layer Change Level Key Changes
Model (Entity) Core (most significant) @Entity→@Table, remove @GeneratedValue/@PrePersist/@ManyToOne, remove UserDetails
Repository Minimal JpaRepository→ReactiveCrudRepository, Optional→Mono, List→Flux
Service Ripple effect Handle @PrePersist/@GeneratedValue manually, reactive chains (flatMap/switchIfEmpty)
Controller Wrapper ResponseEntity→Mono<ResponseEntity>, use .map()/.defaultIfEmpty()

Detailed Documentation by Layer

1. Model (Entity & DTO)

Status: Documented
Core changes at the data model level including R2DBC annotations, removing JPA lifecycle callbacks, and architectural decisions.

2. Repository

Status: Documented
Interface changes from JpaRepository to ReactiveCrudRepository, return type changes, and native SQL queries.

3. Service

Status: Documented
Reactive programming patterns, manual handling of JPA callbacks, and reactive chain operators.

4. Controller

Status: Documented
Reactive response handling, Mono wrappers, and WebFlux endpoint patterns.


Quick Reference — Before/After

Dependencies (pom.xml)

<!-- Before (Servlet/JPA) -->
<dependency>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <artifactId>postgresql</artifactId>
</dependency>

<!-- After (WebFlux/R2DBC) -->
<dependency>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
    <artifactId>r2dbc-postgresql</artifactId>
</dependency>

Configuration (application.yml)

# Before (JDBC)
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/db
    username: user
    password: pass

# After (R2DBC)
spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/db
    username: user
    password: pass

R2DBC Limitations

R2DBC is simpler than JPA — no “magic” lifecycle callbacks:

JPA Feature R2DBC Support Workaround
@PrePersist Not supported Set values in Service before save()
@GeneratedValue Not supported Generate UUID manually in Service
@ManyToOne / @OneToMany Not supported Use foreign key (UUID) instead
JPQL Not supported Use native SQL with @Query
Lazy Loading Not supported Manual joins or separate queries

Lessons Learned

  1. Model drives everything — Get entity changes right first, other layers follow
  2. R2DBC is simpler — Less magic means more explicit code in service layer
  3. Reactive chains take practice — flatMap, switchIfEmpty, then, thenReturn
  4. Testing changes too — Use StepVerifier for reactive testing
  5. Separation of concerns — Good opportunity to clean up (e.g., remove UserDetails from entity)

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top