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-webspring-boot-starter-webflux
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
spring-boot-starter-webspring-boot-starter-webfluxspring-boot-starter-webAnnotated @RestController
The annotated model looks almost identical to Spring MVC: just change return types to Mono<T>Flux<T>
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 RouterFunctionHandlerFunction
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
<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>
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@RestControllerAdviceMono<ResponseEntity>
@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 Fluxtext/event-stream
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.
/*
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.
*/
Mono.fromCallable(() -> jdbcCall()).subscribeOn(Schedulers.boundedElastic())