Spring WebFlux: Reactive REST APIs

Spring WebFlux is Spring's reactive web framework, built on Project Reactor. Unlike Spring MVC (which blocks one thread per request), WebFlux uses an event loop that handles thousands of concurrent requests with a small thread pool. WebFlux supports two programming models: the annotated @RestController style (familiar from MVC) and a functional router+handler DSL.

Contents

Dependency & Starter

Replace spring-boot-starter-web with spring-boot-starter-webflux. Spring Boot will start Netty (not Tomcat) as the embedded server.

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Having both spring-boot-starter-web and spring-boot-starter-webflux on the classpath defaults to Spring MVC. Remove spring-boot-starter-web if you want a fully reactive application.

Annotated @RestController

The annotated model looks almost identical to Spring MVC: just change return types to Mono<T> or Flux<T> instead of plain objects.

Java
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;

@RestController
@RequestMapping("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping
    public Flux<Order> getAllOrders() {
        return orderService.findAll();          // returns Flux
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<Order>> getOrder(@PathVariable String id) {
        return orderService.findById(id)
            .map(ResponseEntity::ok)
            .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Order> createOrder(@RequestBody Order order) {
        return orderService.save(order);        // returns Mono
    }

    @DeleteMapping("/{id}")
    public Mono<Void> deleteOrder(@PathVariable String id) {
        return orderService.deleteById(id);
    }
}

Functional Router DSL

The functional model separates routing from handling. A RouterFunction bean maps requests, and a HandlerFunction processes them. This style is more composable and unit-testable.

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.*;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

@Configuration
public class OrderRouter {

    @Bean
    public RouterFunction<ServerResponse> orderRoutes(OrderHandler handler) {
        return route()
            .GET( "/orders",     handler::getAll)
            .GET( "/orders/{id}", handler::getById)
            .POST("/orders",     handler::create)
            .PUT( "/orders/{id}", handler::update)
            .DELETE("/orders/{id}", handler::delete)
            .build();
    }
}

@Component
public class OrderHandler {

    private final OrderService orderService;

    public OrderHandler(OrderService orderService) {
        this.orderService = orderService;
    }

    public Mono<ServerResponse> getAll(ServerRequest request) {
        return ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(orderService.findAll(), Order.class);
    }

    public Mono<ServerResponse> getById(ServerRequest request) {
        String id = request.pathVariable("id");
        return orderService.findById(id)
            .flatMap(order -> ServerResponse.ok().bodyValue(order))
            .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> create(ServerRequest request) {
        return request.bodyToMono(Order.class)
            .flatMap(orderService::save)
            .flatMap(saved -> ServerResponse.status(HttpStatus.CREATED).bodyValue(saved));
    }
}

Reactive Repositories (R2DBC)

Use R2DBC (Reactive Relational Database Connectivity) to access relational databases without blocking. Extend ReactiveCrudRepository for a non-blocking data layer.

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-h2</artifactId>
    <scope>runtime</scope>
</dependency>
Java
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

@Table("orders")
public class Order {
    @Id private String id;
    private String product;
    private String status;
    // getters/setters
}

public interface OrderRepository extends ReactiveCrudRepository<Order, String> {
    Flux<Order> findByStatus(String status);
}

// In the service:
public Flux<Order> findAll() {
    return orderRepository.findAll();   // returns Flux, no blocking
}

Error Handling

Use @ExceptionHandler in a @RestControllerAdvice class, returning a Mono<ResponseEntity> for reactive error responses.

Java
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    public Mono<ResponseEntity<ErrorResponse>> handleNotFound(OrderNotFoundException ex) {
        return Mono.just(
            ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("ORDER_NOT_FOUND", ex.getMessage()))
        );
    }

    @ExceptionHandler(Exception.class)
    public Mono<ResponseEntity<ErrorResponse>> handleGeneric(Exception ex) {
        return Mono.just(
            ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
        );
    }
}

Server-Sent Events (SSE)

WebFlux makes SSE easy: return a Flux with media type text/event-stream and Spring streams each emitted item as an SSE event.

Java
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import reactor.core.publisher.Flux;
import java.time.Duration;

@GetMapping(value = "/orders/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<Order>> streamOrders() {
    return orderService.findAll()
        .delayElements(Duration.ofMillis(500))  // simulate streaming
        .map(order -> ServerSentEvent.<Order>builder()
            .id(order.getId())
            .event("order-update")
            .data(order)
            .build());
}

Spring MVC vs WebFlux

WebFlux does not always outperform MVC. The reactive model pays off mainly under high concurrency with I/O-bound workloads.

Java
/*
  Spring MVC                        Spring WebFlux
  ────────────────────────────────────────────────────────
  Blocking, thread-per-request      Non-blocking, event loop
  Simpler mental model              Steeper learning curve
  Wide ecosystem (JPA, JDBC, etc.)  Requires reactive drivers
  Good for: CRUD apps, low-medium   Good for: high-throughput,
  concurrency, JPA integration      streaming, microservices
                                    calling many async services
  ────────────────────────────────────────────────────────

  Rule of thumb:
  • Use MVC if your team is not reactive-native, or you
    rely heavily on blocking libraries (JPA, JDBC).
  • Use WebFlux for high-concurrency I/O-bound services
    or services that aggregate many downstream calls.
*/
Mixing blocking code (e.g. a JDBC call) inside a reactive pipeline stalls the Netty event loop thread and can deadlock the application. Always offload blocking calls to a dedicated scheduler: Mono.fromCallable(() -> jdbcCall()).subscribeOn(Schedulers.boundedElastic()).