skip to content

How do functional (RouterFunction) endpoints compare to annotated @RestController endpoints, and when would you pick each?

level: seniorimportance: should knowfreq 40%

answer

  1. same DispatcherHandler, two front-ends
  2. RequestMappingHandlerMapping vs RouterFunctionMapping
  3. annotated: @Valid/@ExceptionHandler/@ControllerAdvice
  4. functional: explicit, composable, no reflection
  5. coexist; not a perf difference

basics

~20 s

Both run on the same reactive engine. Annotated controllers are declarative and feature-rich; functional endpoints put routing in explicit, composable code with no reflection. You can mix both in one app; choose by team preference, need for programmatic composition, and reliance on annotation-only features.

solid answer

~40 s

Functional endpoints and annotated controllers are two front-ends over the same WebFlux runtime (DispatcherHandler, same reactive stack). Annotated (@RestController/@GetMapping) is declarative, discovered by reflection via RequestMappingHandlerMapping, and has the richest built-in support: @ExceptionHandler, @ControllerAdvice, argument resolvers, @Valid, content negotiation, etc. Functional (RouterFunction<ServerResponse> beans) makes routing explicit imperative code: predicates and handlers are first-class values you can compose, nest, filter, and unit-test without reflection or a running server. They coexist in one application — RouterFunctionMapping and RequestMappingHandlerMapping are separate, ordered HandlerMappings. Pick functional when you want visible, composable routing, lightweight handlers, or a more functional style; pick annotated for familiarity, @Valid/@ExceptionHandler ergonomics, and large teams. Most codebases default to annotated and reach for functional selectively.

code

java · 23 lines
java
// Same endpoint, two models — both valid WebFlux, both can live in one app.

// (a) Annotated
@RestController
class PersonController {
    private final PersonService service;
    PersonController(PersonService service) { this.service = service; }
    @GetMapping("/persons/{id}")
    Mono<Person> get(@PathVariable String id) { return service.find(id); }
}

// (b) Functional
@Configuration
class PersonRouter {
    @Bean
    RouterFunction<ServerResponse> routes(PersonService service) {
        return RouterFunctions.route()
            .GET("/fn/persons/{id}", req -> service.find(req.pathVariable("id"))
                .flatMap(p -> ServerResponse.ok().bodyValue(p))
                .switchIfEmpty(ServerResponse.notFound().build()))
            .build();
    }
}

go deeper

for a junior

Knows both models exist and target the same runtime.

for a middle

Can list concrete tradeoffs (validation, exception handling, composability) and write either.

for a senior

Reasons about dispatch internals, coexistence/ordering, and gives sound when-to-use guidance.

for a principal

Sets architectural policy on which model to use where, and how cross-cutting concerns map across both.

**Same engine, two programming models.** Spring WebFlux supports (a) **annotated controllers** — `@RestController` with `@GetMapping` etc. — and (b) **functional endpoints** — `RouterFunction<ServerResponse>` beans. Both are served by the same `DispatcherHandler` over the same reactive stack (Reactor Netty by default). The choice is about the developer-facing model, not runtime capability. **How each is dispatched.** Annotated handlers are found by `RequestMappingHandlerMapping`, which reflectively scans beans for `@RequestMapping` metadata and invokes methods via `RequestMappingHandlerAdapter`. Functional routes are found by `RouterFunctionMapping`, which asks each combined `RouterFunction` `Mono<HandlerFunction> route(ServerRequest)` and invokes the result via `HandlerFunctionAdapter`. Both `HandlerMapping`s live in the same context and are consulted in order — so the two models genuinely coexist in one app. **Annotated — strengths:** - Declarative and familiar; least ceremony for CRUD. - Rich ecosystem: `@ExceptionHandler`, `@ControllerAdvice`, `@Valid`/`@Validated` bean validation, flexible method-argument resolution (`@RequestParam`, `@RequestBody`, `@PathVariable`, `Principal`, etc.), content negotiation, `@ResponseStatus`. - Springdoc/OpenAPI tooling has first-class support. **Annotated — costs:** routing is spread across many classes and hidden in annotations; behavior relies on reflection and convention ('magic'); harder to compose routes programmatically. **Functional — strengths:** - **Explicit routing** in one place; the route table is readable code. - **Composability**: predicates and handlers are values — combine with `.and()/.or()`, group with `nest()`, wrap with `filter()`. You can build routes conditionally at runtime. - **No reflection** for routing; easy, fast unit testing via `WebTestClient.bindToRouterFunction`. - Encourages small, focused handler functions and a functional style that pairs naturally with reactive code. **Functional — costs:** - More boilerplate for simple cases. - No `@ExceptionHandler`/`@ControllerAdvice` — you handle errors with `onErrorResume` in handlers or `.onError()`/filter at the router level. - No automatic `@Valid`; you validate explicitly (e.g. call a `Validator` in the handler). - Less mature docs/tooling and fewer examples than annotated. **Coexistence and ordering.** Because they're separate `HandlerMapping`s, a request is matched by whichever mapping claims it. The two mappings have defined orders, so if (unusually) a path is claimable by both, ordering decides. Practically, teams give each model distinct paths to avoid ambiguity. **Decision guidance:** - Default to **annotated** for most business apps, big teams, or when you lean on `@Valid`/`@ExceptionHandler` and OpenAPI generation. - Choose **functional** for gateways/edge routing, dynamically composed or programmatic route tables, lightweight microservices, a preference for explicit imperative wiring, or when the team is comfortable with reactive/functional idioms. - **Mix** deliberately: e.g. functional for a small set of high-throughput or specially-filtered routes, annotated for the bulk CRUD. **Gotchas / misconceptions:** - Functional endpoints are NOT more performant by default — same runtime; benefits are structural, not throughput. - They are NOT 'the replacement' for annotations; Spring maintains both as peers. - Filters in the functional model (`HandlerFilterFunction`) are per-router, distinct from `WebFilter` (global, applies to both models).

  • In the functional model, what replaces @ExceptionHandler / @ControllerAdvice?
    Reactive error handling inside handlers (onErrorResume/onErrorReturn) and router-level HandlerFilterFunctions via .filter()/.onError(); there is no @ExceptionHandler in functional endpoints.
  • Is functional routing faster than annotated?
    Not inherently — both share the same reactive runtime. Functional avoids reflective dispatch setup, but the benefit is structural (explicitness, composability, testability), not meaningful throughput gains.

saying these in an interview costs you the question

  • Claiming functional endpoints are significantly faster or 'the future replacement' for annotations.
  • Thinking the two models can't run in the same app.
  • Expecting @Valid or @ExceptionHandler to work with RouterFunctions.
  • Assuming a HandlerFilterFunction is the same as a global WebFilter.

context