Spring WebFlux
Spring WebFlux is Spring’s reactive web framework, the non-blocking alternative to Spring MVC. It is built on Project Reactor and runs by default on Netty, an asynchronous event-loop server. WebFlux offers two programming models for the same engine: annotated controllers (familiar to MVC users) and a functional routing style.
Adding the starter
A single starter brings in WebFlux, Reactor, and an embedded Netty server.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Warning: Do not put both
spring-boot-starter-webandspring-boot-starter-webfluxon the classpath expecting WebFlux to win — Spring Boot detectsspring-web(servlet) and starts Tomcat in MVC mode instead. Pick one stack per application.
When WebFlux starts you will see Netty, not Tomcat, in the logs:
Netty started on port 8080 (http)
Started Application in 1.42 seconds (process running for 1.7)
Annotated controllers
The annotations are the same ones you know from Spring MVC — @RestController, @GetMapping, @PathVariable, @RequestBody. The difference is the return type: handlers return a Mono or Flux instead of a plain value or List.
@RestController
@RequestMapping("/api/users")
class UserController {
private final UserService service;
UserController(UserService service) {
this.service = service;
}
@GetMapping("/{id}")
Mono<User> byId(@PathVariable String id) {
return service.findById(id); // 0..1
}
@GetMapping
Flux<User> all() {
return service.findAll(); // 0..N
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Mono<User> create(@RequestBody Mono<User> body) {
return body.flatMap(service::save); // body itself is reactive
}
}
Spring subscribes to the returned publisher and streams the result to the response. Notice the request body can also be a Mono<User> — it is deserialized reactively as bytes arrive.
Functional routing
The functional model defines routes as data using RouterFunction and HandlerFunction, with no annotations. Handlers take a ServerRequest and return a Mono<ServerResponse>. This style keeps routing explicit and centralized.
@Configuration
class UserRouter {
@Bean
RouterFunction<ServerResponse> routes(UserHandler handler) {
return RouterFunctions.route()
.GET("/fn/users/{id}", handler::byId)
.GET("/fn/users", handler::all)
.POST("/fn/users", handler::create)
.build();
}
}
@Component
class UserHandler {
private final UserService service;
UserHandler(UserService service) {
this.service = service;
}
Mono<ServerResponse> byId(ServerRequest request) {
String id = request.pathVariable("id");
return service.findById(id)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
Mono<ServerResponse> all(ServerRequest request) {
return ServerResponse.ok().body(service.findAll(), User.class);
}
Mono<ServerResponse> create(ServerRequest request) {
return request.bodyToMono(User.class)
.flatMap(service::save)
.flatMap(saved -> ServerResponse.status(HttpStatus.CREATED).bodyValue(saved));
}
}
Choosing a style
| Annotated controllers | Functional routing | |
|---|---|---|
| Familiarity | high (same as MVC) | lower, more explicit |
| Routing | scattered across classes | centralized in router beans |
| Boilerplate | less | more (build ServerResponse by hand) |
| Best for | teams coming from MVC | lightweight services, fine-grained control |
Both run on the same WebFlux engine and can coexist in one application. Most teams start with annotated controllers.
The Netty event loop
Netty serves requests on a small set of event loop threads (by default, one per CPU core). These threads must never block. Everything in a handler — database access, downstream HTTP calls — must be non-blocking, returning a Mono or Flux.
// CATASTROPHE: a blocking call on a Netty event loop thread
@GetMapping("/bad/{id}")
Mono<User> bad(@PathVariable Long id) {
User u = jdbcRepository.findById(id).orElseThrow(); // BLOCKS the event loop!
return Mono.just(u);
}
A single blocking call here stalls the loop for every concurrent request it was serving. If you genuinely must call blocking code, offload it with subscribeOn(Schedulers.boundedElastic()) — but the right fix is a reactive driver like R2DBC and a reactive HTTP client like WebClient.
Note: WebFlux can also run on a Servlet 3.1+ container (Tomcat, Jetty) in non-blocking mode if you add that server, but Netty is the default and the most natural fit.
Configuration
server:
port: 8080
spring:
webflux:
base-path: /api # context path for all routes
Related Topics
- Reactive Programming — the model behind WebFlux.
- Mono & Flux — the return types every handler uses.
- Reactive REST APIs — full CRUD and streaming endpoints.
- WebFlux vs Spring MVC — when to pick the reactive stack.
- Building REST APIs — the blocking MVC equivalent.