skip to content

How do you register functional routes as a bean and organize handler logic separately from routing?

level: middleimportance: should knowfreq 45%

answer

  1. router config = thin, handler = logic
  2. method references h::getById
  3. @Component handler, constructor-injected service
  4. RouterFunctionMapping auto-detects beans
  5. switchIfEmpty(notFound)

basics

~10 s

Expose a @Bean of type RouterFunction<ServerResponse> in a @Configuration class that only wires paths to handlers, and put the actual logic in a separate @Component 'handler' class whose methods are referenced by method reference.

solid answer

~40 s

The idiomatic split is: a router @Configuration class holds one @Bean RouterFunction<ServerResponse> that maps predicates to handler method references, and a separate @Component handler class contains the methods (each ServerRequest -> Mono<ServerResponse>). RouterFunctionMapping auto-detects the bean — no explicit registration. This keeps routing declarative and readable while handlers stay unit-testable and injectable (they can depend on services/repositories via constructor injection). You can define multiple RouterFunction beans across features; Spring combines them all. Handlers do request parsing (pathVariable, bodyToMono), call the service layer, and build a ServerResponse. Because the handler is just a bean, you test it by passing a mock ServerRequest, or test the whole router with WebTestClient.bindToRouterFunction without starting a server.

code

java · 36 lines
java
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import org.springframework.web.reactive.function.server.*;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Configuration
class PersonRouterConfig {
    @Bean
    RouterFunction<ServerResponse> personRoutes(PersonHandler h) {
        return RouterFunctions.route()
            .GET("/persons/{id}", accept(APPLICATION_JSON), h::getById)
            .POST("/persons", h::create)
            .build();
    }
}

@Component
class PersonHandler {
    private final PersonService service;
    PersonHandler(PersonService service) { this.service = service; }

    Mono<ServerResponse> getById(ServerRequest req) {
        return service.find(req.pathVariable("id"))
            .flatMap(p -> ServerResponse.ok().bodyValue(p))
            .switchIfEmpty(ServerResponse.notFound().build());
    }

    Mono<ServerResponse> create(ServerRequest req) {
        return req.bodyToMono(Person.class)
            .flatMap(service::save)
            .flatMap(saved -> ServerResponse.created(req.uriBuilder().path("/{id}").build(saved.id()))
                                            .bodyValue(saved));
    }
}

go deeper

for a junior

Can register a single @Bean RouterFunction and inline a handler.

for a middle

Applies the router/handler split, uses method references, injects services into the handler component.

for a senior

Reasons about multiple beans, ordering, reactive error handling, and testability tradeoffs.

for a principal

Sets team conventions for structuring functional endpoints and integrating with the service/domain layers.

**Why split router from handler?** Putting routing and business logic in the same lambda quickly becomes unreadable and untestable. The community-standard pattern mirrors 'controller vs service': a thin **router configuration** and a **handler component**. **1. The router bean.** A `@Configuration` class declares a method annotated `@Bean` returning `RouterFunction<ServerResponse>`. It receives the handler as a parameter (Spring injects it) and only wires predicates to handler methods via method references: ```java @Bean RouterFunction<ServerResponse> personRoutes(PersonHandler h) { return route() .GET("/persons/{id}", h::getById) .GET("/persons", h::list) .POST("/persons", h::create) .build(); } ``` **2. The handler component.** A `@Component` (often named `...Handler`) holds the methods. Each has the `HandlerFunction` shape `Mono<ServerResponse> m(ServerRequest)` and depends on services via constructor injection: ```java @Component class PersonHandler { private final PersonService service; PersonHandler(PersonService service) { this.service = service; } Mono<ServerResponse> getById(ServerRequest req) { return service.find(req.pathVariable("id")) .flatMap(p -> ServerResponse.ok().bodyValue(p)) .switchIfEmpty(ServerResponse.notFound().build()); } } ``` **3. Automatic discovery.** You never call anything to 'install' the routes. `RouterFunctionMapping`, auto-configured by Spring Boot's WebFlux support, scans the `ApplicationContext` for every bean of type `RouterFunction<ServerResponse>`, combines them (in bean order), and inserts them into the `DispatcherHandler` dispatch chain. Adding a new feature = adding another `@Bean RouterFunction<ServerResponse>`; no central registry to edit. **4. Reading and writing.** `ServerRequest` gives functional access: `pathVariable`, `queryParam`, `headers()`, and reactive body access `bodyToMono(Type.class)` / `bodyToFlux(Type.class)`. `ServerResponse` is a fluent builder terminating in `Mono<ServerResponse>`: `ok().bodyValue(obj)` for a materialized value, `.body(publisher, Type.class)` to stream a reactive source, `status(...).build()` for empty bodies, plus `.contentType(...)`, `.header(...)`, `.cookie(...)`. **5. Handling 'not found' / empty.** A common idiom is `switchIfEmpty(ServerResponse.notFound().build())` on a `Mono` that may complete empty, and `onErrorResume` for error mapping — since there is no `@ExceptionHandler` in the functional model (see filters/`onError` for cross-cutting error handling). **Gotchas:** - The handler methods must be reachable as method references with the exact `ServerRequest -> Mono<ServerResponse>` signature. - Don't inject blocking repositories and block inside a handler — stay reactive (`flatMap`, not `.block()`). - Multiple beans work, but if two routes match the same request, the one whose bean is combined first wins — control it with `@Order` on the beans or careful predicate design. - The handler doesn't need `@Component` if you instantiate it directly in the config method, but a bean makes it independently testable and injectable. **When to use:** any non-trivial functional-endpoint app. The split is essentially mandatory for maintainability once you pass a couple of routes.

  • Do you have to manually register the RouterFunction bean anywhere?
    No. RouterFunctionMapping automatically discovers all RouterFunction<ServerResponse> beans in the context and wires them into dispatch.
  • How would you test these routes without a running server?
    Use WebTestClient.bindToRouterFunction(router).build(), which exercises the RouterFunction directly in-memory.

saying these in an interview costs you the question

  • Calling .block() inside a handler instead of staying reactive.
  • Putting all business logic in the router lambdas.
  • Thinking you must @Autowire or list the RouterFunction in some registry.
  • Expecting @ExceptionHandler to work in the functional model.

context