How does the same DispatcherHandler pipeline serve both annotated @Controller and functional RouterFunction endpoints? What are the design trade-offs?
answer
- One runtime, two models
- Triad per model, selected by supports()
- Open/closed front controller = strategy pattern
- Annotated = declarative/reflective; functional = explicit/composable
- Shared filters/codecs, different result handlers
basics
~10 sBoth 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 sWebFlux 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// 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
Know both annotated and functional styles exist in WebFlux.
Map each model to its HandlerMapping/Adapter and note they share the runtime.
Explain the supports()-based selection and that both share filters, codecs, and exchange.
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