Contents
- Dependency & Starter
- Annotated @RestController
- Functional Router DSL
- Reactive Repositories (R2DBC)
- Error Handling
- Server-Sent Events (SSE)
- Spring MVC vs WebFlux
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()).