Home / Spring Boot / Web and REST / Reactive Programming with Spring WebFlux

Reactive Programming with Spring WebFlux

WebFlux is the non-blocking web stack of Spring. It handles many concurrent connections with a small number of threads, using Project Reactor types Mono (0 or 1 element) and Flux (0 to N elements). Understand when it helps and when virtual threads are simpler.

Thread per request vs event loop

SPRING MVC (blocking)                              SPRING WEBFLUX (non-blocking)
 request 1 ─► thread 1 ── waits for DB ──►           request 1 ┐
 request 2 ─► thread 2 ── waits for API ─►           request 2 ├─► few event-loop threads (about CPU count)
 request 3 ─► thread 3 ── waits ─────────►           request 3 ┘   never wait: they register a callback and move on
 1000 waiting requests = 1000 threads                1000 waiting requests = still a few threads

Mono and Flux

A Mono or Flux is a description of work (a pipeline). Nothing happens until someone subscribes. WebFlux subscribes for you when you return the Mono or Flux from a controller.

Mono<String> one = Mono.just("Hello");
Flux<Integer> many = Flux.range(1, 5);

Flux<String> result = Flux.just("ram", "sita", "amit")
        .filter(n -> n.length() > 3)
        .map(String::toUpperCase)
        .doOnNext(n -> System.out.println("processing " + n));   // nothing printed yet

result.subscribe(System.out::println);                            // now it runs: SITA, AMIT
OperatorMeaning
mapTransform each element (synchronous)
flatMapTransform each element into a Mono or Flux (asynchronous), and merge the results
filter, take, skipSelect elements
zip / Mono.zipCombine results of independent calls (run in parallel)
onErrorResume, onErrorReturnRecover from errors
switchIfEmptyProvide an alternative when empty
timeout, retryWhenResilience
collectListTurn a Flux into a Mono of a list

A WebFlux controller with WebClient

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
</dependency>
@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {

    private final WebClient users = WebClient.create("http://user-service");
    private final WebClient orders = WebClient.create("http://order-service");

    public record Dashboard(User user, List<Order> orders) { }

    @GetMapping("/{userId}")
    public Mono<Dashboard> dashboard(@PathVariable String userId) {
        Mono<User> user = users.get().uri("/users/{id}", userId).retrieve().bodyToMono(User.class);
        Mono<List<Order>> list = orders.get().uri("/orders?user={id}", userId)
                .retrieve().bodyToFlux(Order.class).collectList();

        return Mono.zip(user, list)                                        // both calls run at the same time
                .map(t -> new Dashboard(t.getT1(), t.getT2()))
                .timeout(Duration.ofSeconds(3))
                .onErrorResume(TimeoutException.class, e -> Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT)));
    }

    @GetMapping(value = "/ticker", produces = MediaType.TEXT_EVENT_STREAM_VALUE)   // Server-Sent Events
    public Flux<String> ticker() {
        return Flux.interval(Duration.ofSeconds(1)).map(i -> "tick " + i);
    }
}

Functional endpoints (alternative to annotations)

@Configuration
public class Routes {
    @Bean
    RouterFunction<ServerResponse> routes() {
        return RouterFunctions.route()
                .GET("/hello/{name}", req -> ServerResponse.ok().bodyValue("Hello " + req.pathVariable("name")))
                .build();
    }
}

Testing with StepVerifier

@Test
void upperCase() {
    Flux<String> flux = Flux.just("a", "b").map(String::toUpperCase);
    StepVerifier.create(flux).expectNext("A").expectNext("B").verifyComplete();
}

Rules of reactive code

WebFlux or virtual threads?

SituationChoice
Normal CRUD with JDBC or JPASpring MVC with virtual threads (simple code, scales well)
Streaming, backpressure, SSE, very many open connectionsWebFlux
API gateway that only forwards requests (Spring Cloud Gateway)WebFlux (it is built on it)
Team new to reactive, blocking librariesMVC. Reactive has a learning cost

Common mistake: Calling a blocking database or HTTP client inside a WebFlux pipeline. It blocks the few event-loop threads, and the whole service becomes slower than a normal MVC application. Stay non-blocking end to end, or use MVC.

Interview questions

What is backpressure?

The consumer tells the producer how many elements it can handle (request(n)), so a fast producer does not overflow a slow consumer. Reactive Streams defines this.

map or flatMap?

map is for a simple synchronous transformation. flatMap is when the transformation itself returns a Mono or Flux (for example another remote call).

Why does nothing happen if I do not subscribe?

Reactive pipelines are lazy. They describe work and execute only when subscribed. WebFlux subscribes when you return the value from a controller.