Contents

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

<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.

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

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); } }

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.

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)); } }

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

<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 }

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

@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")) ); } }

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

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()); }

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. */ 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()).