Skip to content
Spring Boot sb reactive 3 min read

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-web and spring-boot-starter-webflux on the classpath expecting WebFlux to win — Spring Boot detects spring-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 controllersFunctional routing
Familiarityhigh (same as MVC)lower, more explicit
Routingscattered across classescentralized in router beans
Boilerplatelessmore (build ServerResponse by hand)
Best forteams coming from MVClightweight 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
Last updated June 13, 2026
Was this helpful?