skip to content

Internally, how does Spring WebFlux dispatch a request to a functional route, and how are multiple RouterFunction beans, predicates, and filters composed and ordered?

level: principalimportance: nice to knowfreq 20%

answer

  1. DispatcherHandler -> ordered HandlerMappings
  2. RouterFunctionMapping combines beans (andOther)
  3. route(request) -> Mono<HandlerFunction>, first match wins
  4. HandlerFunctionAdapter + ServerResponseResultHandler
  5. HandlerFilterFunction (per-router) vs WebFilter (global)

basics

~10 s

RouterFunctionMapping collects all RouterFunction<ServerResponse> beans, combines them, and for each request calls RouterFunction.route(request) to get a Mono<HandlerFunction>. HandlerFunctionAdapter invokes it; ServerResponse writes the result. Routes are tried in order; first match wins.

solid answer

~30 s

At startup RouterFunctionMapping (a HandlerMapping) gathers every RouterFunction<ServerResponse> bean and combines them into one composite via RouterFunction.andOther, in bean order (influenced by @Order). Per request, DispatcherHandler consults its ordered HandlerMappings; RouterFunctionMapping calls composite.route(ServerRequest), which walks routes in declaration/combination order calling each RequestPredicate.test — the first match returns a Mono<HandlerFunction>. The matched HandlerFunction is the handler, adapted by HandlerFunctionAdapter, and its Mono<ServerResponse> is rendered by ServerResponseResultHandler (writeTo). filter() wraps handlers as HandlerFilterFunction (per-router, distinct from global WebFilter). Predicates compose via and/or/negate; nest() ANDs a shared predicate and can short-circuit. Ordering across the annotated RequestMappingHandlerMapping is by HandlerMapping order.

code

java · 29 lines
java
// Combining beans, ordering, group filter, and a custom predicate.
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import org.springframework.web.reactive.function.server.*;
import org.springframework.core.annotation.Order;
import org.springframework.context.annotation.*;

@Configuration
class RoutingConfig {

    @Bean @Order(1) // consulted before the catch-all bean when paths overlap
    RouterFunction<ServerResponse> apiRoutes(ApiHandler h) {
        return RouterFunctions.route()
            .nest(path("/api").and(accept(org.springframework.http.MediaType.APPLICATION_JSON)), b -> b
                .GET("/items/{id}", h::getItem)
                .GET("/items", h::listItems)
                // HandlerFilterFunction scoped to this nest only
                .filter((request, next) ->
                    next.handle(request)
                        .onErrorResume(ex -> ServerResponse.status(500).bodyValue("failed"))))
            .build();
    }

    @Bean @Order(2)
    RouterFunction<ServerResponse> fallback(ApiHandler h) {
        return RouterFunctions.route()
            .GET("/**", h::notFound) // catch-all last
            .build();
    }
}

go deeper

for a junior

Aware there is some mapping that finds the handler; details not expected.

for a middle

Knows RouterFunctionMapping discovers beans and first-match-wins ordering.

for a senior

Can trace route()->HandlerFunctionAdapter->ServerResponseResultHandler and explain filters vs WebFilter.

for a principal

Reasons end-to-end about composition, ordering across HandlerMappings, short-circuiting nests, and non-blocking predicate/handler discipline.

**The dispatch pipeline.** WebFlux's front controller is `DispatcherHandler` (the reactive analog of MVC's `DispatcherServlet`). It holds ordered lists of `HandlerMapping`, `HandlerAdapter`, and `HandlerResultHandler` beans. For each `ServerWebExchange` it: (1) asks each `HandlerMapping` in order for a handler, taking the first non-empty; (2) finds a `HandlerAdapter` that `supports(handler)` and calls `handle(...)` to get a `Mono<HandlerResult>`; (3) finds a `HandlerResultHandler` that `supports(result)` to write the response. **Where functional routes plug in.** - **`RouterFunctionMapping`** is the `HandlerMapping` for functional endpoints. On init it finds all `RouterFunction<ServerResponse>` beans in the context and **combines** them into a single composite `RouterFunction` (equivalent to chaining `RouterFunction.andOther(...)`), preserving bean order. Bean order can be influenced with `@Order` / `Ordered`. - Per request it wraps the exchange in a `ServerRequest` and calls `composite.route(serverRequest)`, which returns `Mono<HandlerFunction<ServerResponse>>`. The composite evaluates constituent routes **in order**: each `RouterFunction` tests its `RequestPredicate` against the `ServerRequest`; the **first predicate that matches** yields its `HandlerFunction` and evaluation stops (first-match-wins). If none match, the `Mono` is empty and `DispatcherHandler` moves on to the next `HandlerMapping`. - The resolved handler is the `HandlerFunction` itself. **`HandlerFunctionAdapter`** is the `HandlerAdapter` that `supports` `HandlerFunction`; it invokes `handlerFunction.handle(serverRequest)` producing `Mono<ServerResponse>`, wrapped as a `HandlerResult`. - **`ServerResponseResultHandler`** is the `HandlerResultHandler` that recognizes a `ServerResponse` result and calls its `writeTo(exchange, context)` to serialize status, headers, and body (using the configured `HttpMessageWriter`s / codecs). **Predicate composition mechanics.** `RequestPredicate` exposes `test(ServerRequest)`, plus combinators `and`, `or`, `negate`. `RequestPredicates` provides factories (`GET`, `POST`, `path`, `pathExtension`, `accept`, `contentType`, `headers`, `queryParam`, `method`). Path matching uses `PathPattern` (the same parser as annotated mappings), so `{var}` capture and `**` behave consistently, and captured variables are stored as request attributes readable via `ServerRequest.pathVariable`. **nest() internals.** `nest(predicate, consumer)` produces a `NestedRouterFunction`-style structure: the outer predicate is tested first; only if it matches are the inner routes evaluated (their predicates effectively ANDed with the outer). This gives a natural **short-circuit**: a large group whose prefix doesn't match is skipped wholesale, and nested path variables from the outer predicate remain available to inner handlers. `path(pattern, consumer)` is `nest(RequestPredicates.path(pattern), consumer)`. **Filters.** `filter(HandlerFilterFunction)` wraps the matched `HandlerFunction`: a `HandlerFilterFunction<T,R>` has `Mono<R> filter(ServerRequest, HandlerFunction<T> next)`, so it can inspect/transform the request, short-circuit, or post-process the response by composing around `next.handle(request)`. `before`/`after`/`onError` are conveniences over it. Crucially these are **per-RouterFunction** and only wrap routes within their builder/nest scope — distinct from a global **`WebFilter`**, which sits in the `WebHandler` chain ahead of `DispatcherHandler` and applies to *all* requests (both functional and annotated). **Ordering across models.** `RouterFunctionMapping` and `RequestMappingHandlerMapping` are both `HandlerMapping`s with `Ordered` values; `DispatcherHandler` consults them in that order and the first to return a handler wins. So if a path could be claimed by both models, the relative `HandlerMapping` order decides — teams typically avoid overlap by giving each distinct paths. Within functional routing, ordering is purely by declaration/combination sequence, which is why more specific routes must precede catch-alls to avoid shadowing. **Practical implications / gotchas:** - Combining many beans: control precedence with `@Order` on the `@Bean RouterFunction` methods when overlap is possible. - `route()` returns a *stateful builder*; reusing predicates across builders is fine because predicates are immutable values. - Because routing is a `Mono`, predicate evaluation itself stays non-blocking; avoid heavyweight/blocking logic in custom `RequestPredicate.test`. - Errors thrown while producing the response should be handled reactively (in the handler or a filter's `onError`) — there's no annotated advice fallback for functional routes.

  • What's the difference between a HandlerFilterFunction and a WebFilter?
    HandlerFilterFunction is functional-endpoint-scoped: it wraps the matched HandlerFunction within a specific RouterFunction/nest via .filter(). A WebFilter is global, sits in the WebHandler chain before DispatcherHandler, and applies to every request regardless of programming model.
  • If two RouterFunction beans could both match a path, how is the winner decided?
    By combination/bean order — RouterFunctionMapping combines beans in order (influenceable via @Order), and route() evaluation is first-match-wins, so the earlier-ordered bean's matching route claims the request.
  • Which components adapt and render a functional handler's result?
    HandlerFunctionAdapter invokes the HandlerFunction to get Mono<ServerResponse>; ServerResponseResultHandler then calls ServerResponse.writeTo to serialize status/headers/body via the configured codecs.

saying these in an interview costs you the question

  • Saying annotated and functional routes are matched by the same HandlerMapping.
  • Claiming all routes are evaluated and 'best match' wins (it's first-match in order).
  • Confusing per-router HandlerFilterFunction with a global WebFilter.
  • Doing blocking work inside a RequestPredicate.test or handler on the event loop.

context