skip to content

How does the same DispatcherHandler pipeline serve both annotated @Controller and functional RouterFunction endpoints? What are the design trade-offs?

level: principalimportance: nice to knowfreq 25%

answer

  1. One runtime, two models
  2. Triad per model, selected by supports()
  3. Open/closed front controller = strategy pattern
  4. Annotated = declarative/reflective; functional = explicit/composable
  5. Shared filters/codecs, different result handlers

basics

~10 s

Both models plug into the same pipeline through different strategy beans: annotated methods use RequestMappingHandlerMapping/Adapter; functional routes use RouterFunctionMapping and HandlerFunctionAdapter. DispatcherHandler doesn't care which — it just picks the mapping/adapter that supports each.

solid answer

~40 s

WebFlux ships two programming models over one runtime. **Annotated** (`@RestController`, `@GetMapping`) is served by `RequestMappingHandlerMapping` (resolves to a `HandlerMethod`) + `RequestMappingHandlerAdapter` (reflective argument resolution + invocation) + `ResponseBodyResultHandler`. **Functional** (`RouterFunctions.route().GET(...).build()`) is served by `RouterFunctionMapping` (resolves to a `HandlerFunction`) + `HandlerFunctionAdapter` (invokes `handle(ServerRequest)` → `Mono<ServerResponse>`) + `ServerResponseResultHandler`. DispatcherHandler holds ordered lists and simply asks each mapping/adapter/result-handler whether it `supports` the request/handler/result — so both models coexist in the same app, even the same URL space. Trade-offs: annotated is declarative, familiar, richer auto-binding/validation; functional is explicit, composable, testable without reflection, and lets routing be a first-class value. DispatcherHandler's strategy design is what makes this pluralism possible without forking the runtime.

code

java · 32 lines
java
// Functional model — routing is a first-class bean
@Configuration
class BookRoutes {
    @Bean
    RouterFunction<ServerResponse> routes(BookHandler handler) {
        return RouterFunctions.route()
            .GET("/fn/books/{id}", handler::getBook)
            .POST("/fn/books", handler::createBook)
            .build();
    }
}

@Component
class BookHandler {
    Mono<ServerResponse> getBook(ServerRequest request) {
        String id = request.pathVariable("id");
        return ServerResponse.ok().bodyValue("book-" + id);
    }
    Mono<ServerResponse> createBook(ServerRequest request) {
        return request.bodyToMono(String.class)
            .flatMap(body -> ServerResponse.status(HttpStatus.CREATED).bodyValue(body));
    }
}

// Annotated model — same app, same DispatcherHandler, different triad
@RestController
class AnnotatedBookController {
    @GetMapping("/books/{id}")
    Mono<String> get(@PathVariable String id) {
        return Mono.just("book-" + id);
    }
}

go deeper

for a junior

Know both annotated and functional styles exist in WebFlux.

for a middle

Map each model to its HandlerMapping/Adapter and note they share the runtime.

for a senior

Explain the supports()-based selection and that both share filters, codecs, and exchange.

for a principal

Frame DispatcherHandler as an open/closed strategy front controller enabling model pluralism; weigh declarative-vs-explicit trade-offs and interop/ordering/error-handling implications.

A principal-level answer frames DispatcherHandler's strategy architecture as the reason WebFlux can offer **two coexisting programming models on one non-blocking runtime**, and weighs the engineering trade-offs. **The two models.** - **Annotated controllers** — `@Controller`/`@RestController` with `@RequestMapping` family annotations. Metadata-driven: Spring scans mappings at startup and binds via reflection at request time. - **Functional endpoints** — `RouterFunction<ServerResponse>` built with `RouterFunctions.route()`, using `RequestPredicate`s and `HandlerFunction`s. Routing and handling are ordinary objects/lambdas you compose and pass around. **How both reach the same pipeline.** DispatcherHandler owns three *ordered lists*. Each model contributes its own triad: | Stage | Annotated | Functional | |-------|-----------|------------| | HandlerMapping | `RequestMappingHandlerMapping` → `HandlerMethod` | `RouterFunctionMapping` → `HandlerFunction` | | HandlerAdapter | `RequestMappingHandlerAdapter` (arg resolvers, reflective invoke) | `HandlerFunctionAdapter` (`handlerFunction.handle(serverRequest)`) | | ResultHandler | `ResponseBodyResultHandler` / `ResponseEntityResultHandler` / `ViewResolutionResultHandler` | `ServerResponseResultHandler` (`ServerResponse.writeTo(exchange, ...)`) | At request time DispatcherHandler iterates the `HandlerMapping`s in order; whichever returns a handler first wins. It then finds the first `HandlerAdapter` whose `supports(handler)` is true, and finally the first `HandlerResultHandler` whose `supports(result)` is true. Because selection is by capability, the two models are fully interoperable — you can run annotated controllers and a `RouterFunction` in the same context, sharing the same `WebFilter`s, codecs, and exception handlers. Ordering (`Ordered`) decides precedence if both could match the same path (rare, but `RouterFunctionMapping` and `RequestMappingHandlerMapping` have defined orders). **Why this design (the principal insight).** DispatcherHandler is an *open/closed* front controller: it's closed to modification but open to extension via new strategy beans. Supporting a new style = registering a mapping+adapter+result-handler triad, never editing the dispatcher. This is the same strategy pattern that let Spring MVC evolve; WebFlux reuses the philosophy on a reactive substrate. **Trade-offs to articulate.** - *Annotated*: pros — declarative, least boilerplate, deep ecosystem (validation via `@Valid`, `@ExceptionHandler`, `@ControllerAdvice`, content negotiation, rich argument resolvers); cons — reflection/metadata magic, harder to unit-test in isolation, routing is scattered across annotations, some runtime cost and less explicit control flow. - *Functional*: pros — explicit and transparent control flow, routing is a first-class composable value (easy to build/compose/nest predicates), lighter and very testable (call the handler with a mock `ServerRequest`, no Spring context needed), good for small/edge services and gateways; cons — more verbose for large APIs, less automatic binding/validation sugar, fewer out-of-the-box cross-cutting hooks (you wire filters/handler-filters manually via `HandlerFilterFunction`). - *Interop*: because both share the same runtime, teams can mix — e.g., functional routes for a lean public edge plus annotated controllers for a large internal API — without runtime cost beyond an extra HandlerMapping in the list. **Gotchas.** - Content negotiation, message codecs, and exception handling are shared, but `@ControllerAdvice`/`@ExceptionHandler` apply only to the annotated model; functional endpoints handle errors inline or via `HandlerFilterFunction`/global `WebExceptionHandler`. - Both ultimately produce writes to the same `ServerHttpResponse` on the same `ServerWebExchange`; a `ServerResponse` is just a higher-level builder that renders via message writers, same as `ResponseBodyResultHandler`. - Ordering surprises: if a path is matched by both a router function and an annotated mapping, the `HandlerMapping` order decides — keep route spaces disjoint to avoid confusion.

  • If a URL is matched by both a RouterFunction and an annotated controller, which wins?
    Whichever HandlerMapping is ordered first returns a handler and short-circuits the iteration (Ordered decides between RouterFunctionMapping and RequestMappingHandlerMapping). Best practice: keep the two route spaces disjoint.
  • Do @ControllerAdvice/@ExceptionHandler apply to functional endpoints?
    No — those are part of the annotated model's adapter. Functional endpoints handle errors inline, via HandlerFilterFunction, or fall through to a global WebExceptionHandler.
  • What makes functional endpoints easier to unit test?
    A HandlerFunction is a plain method taking a ServerRequest and returning Mono<ServerResponse>; you can call it with a mock/MockServerRequest and assert on the response with no Spring context or reflection.

saying these in an interview costs you the question

  • Claiming you must pick annotated OR functional for the whole app
  • Saying functional endpoints bypass DispatcherHandler
  • Assuming @ControllerAdvice covers functional routes
  • Thinking each model needs its own runtime/server

context