skip to content

Annotated & Functional Endpoints

Writing reactive handlers: annotated controllers returning Mono or Flux, the functional router and handler functions, request predicates, Kotlin coroutines, and streaming multipart. Interviewers usually ask you to compare the annotated and functional styles.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

30

How do you write an annotated reactive controller in Spring WebFlux, and what changes compared to a Spring MVC controller?

level: juniorimportance: must knowfreq 75%

answer

  1. Same annotations as MVC
  2. Return Mono (0-1) / Flux (0-N)
  3. Framework subscribes for you
  4. Empty Mono = 200 empty, not 404
  5. Never .block() on event loop

basics

~10 s

Use the same annotations as MVC (@RestController, @GetMapping, @PathVariable), but return reactive types: Mono<T> for zero-or-one value and Flux<T> for a stream of values, instead of a plain object.

solid answer

~40 s

WebFlux reuses the exact same annotation programming model as MVC: @RestController, @GetMapping/@PostMapping, @PathVariable, @RequestParam, @RequestBody. The one real change is the return type — instead of returning a plain object (or void), you return a Reactor publisher: Mono<T> for a single or empty result, Flux<T> for many. The framework subscribes to that publisher for you and writes the result to the response without blocking a thread. You can still return a plain T (it gets wrapped) or wrap in ResponseEntity. Because everything runs on a small event-loop thread pool (Reactor Netty), the golden rule is: never call .block() or do blocking I/O inside the method — compose everything through the reactive pipeline instead. Same mental model, non-blocking return types.

code

java · 20 lines
java
@RestController
@RequestMapping("/users")
class UserController {

    private final UserRepository repo; // reactive: returns Mono/Flux

    UserController(UserRepository repo) { this.repo = repo; }

    @GetMapping("/{id}")
    Mono<User> byId(@PathVariable String id) {
        return repo.findById(id)
                   .switchIfEmpty(Mono.error(
                       new ResponseStatusException(HttpStatus.NOT_FOUND)));
    }

    @GetMapping
    Flux<User> all() {
        return repo.findAll();
    }
}

go deeper

for a junior

Know that the annotations are the same and only the return type becomes Mono/Flux.

for a middle

Explain the empty-Mono 200-vs-404 gotcha and why blocking is forbidden on the event loop.

for a senior

Discuss offloading blocking work with boundedElastic and when MVC is the better call.

for a principal

Frame the choice around the whole-stack reactive requirement and concurrency/thread-economy trade-offs.

## The core idea Spring WebFlux is Spring's non-blocking, reactive web stack. Its biggest selling point for developers is that the **annotated-controller programming model is identical to Spring MVC**. You use `@RestController`, `@Controller`, `@RequestMapping`, `@GetMapping`, `@PostMapping`, `@PathVariable`, `@RequestParam`, `@RequestBody`, `@RequestHeader`, `@ResponseStatus`, `@ExceptionHandler` — the same annotations, resolved by the same `HandlerMapping`/`HandlerAdapter` abstractions. ## What actually changes: the return type In MVC, a handler returns a plain value like `User` or `List<User>`, and the framework blocks the request thread until you produce it. In WebFlux you return a **Reactive Streams `Publisher`**, almost always one of Project Reactor's two types: - **`Mono<T>`** — a publisher that emits **0 or 1** item then completes (or errors). Use for a single resource, an empty result, or a `void`-like operation (`Mono<Void>`). - **`Flux<T>`** — a publisher that emits **0..N** items then completes (or errors). Use for collections/streams. The framework **subscribes** to whatever you return, and only when data actually arrives does it get written to the HTTP response. Nothing runs on a blocked thread waiting. ## Terms defined - **Reactive / non-blocking**: instead of a thread parking while it waits for I/O (DB, HTTP call), the thread is released and a callback resumes work when data is ready. A handful of event-loop threads serve many concurrent requests. - **Publisher / subscribe**: a `Publisher` produces nothing until something *subscribes*. In annotated WebFlux controllers, **Spring subscribes for you** — you never call `.subscribe()` yourself. - **Reactor Netty**: the default runtime server; it uses a small fixed set of event-loop threads. ## A minimal example ```java @RestController @RequestMapping("/users") class UserController { private final UserRepository repo; // reactive repo returns Mono/Flux @GetMapping("/{id}") Mono<User> byId(@PathVariable String id) { return repo.findById(id); // Mono<User>, may be empty } @GetMapping Flux<User> all() { return repo.findAll(); // Flux<User> } } ``` ## Allowed return shapes - `Mono<T>` / `Flux<T>` (Reactor) — the idiomatic choice. - Any Reactive Streams `Publisher`, RxJava types, Kotlin `Flow`, or coroutine `suspend` functions. - A **plain `T`** — Spring wraps it; fine when the value is computed synchronously and cheaply. - `Mono<ResponseEntity<T>>` or `ResponseEntity<Mono<T>>` when you need to set status/headers. ## Empty-result edge case An empty `Mono<User>` (repository found nothing) results in a **200 OK with an empty body** by default — NOT a 404. To turn 'not found' into a 404 you must handle it explicitly, e.g. `.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)))` or return `Mono<ResponseEntity<T>>` mapping empty to `notFound()`. ## The one hard rule Because only a few event-loop threads exist, **blocking one stalls many requests**. Never call `.block()`, `Thread.sleep`, JDBC, or any blocking library directly inside a WebFlux controller. If you must call blocking code, offload it with `Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())`. ## When to use WebFlux annotated controllers shine when the whole stack is non-blocking (R2DBC, WebClient) and you need high concurrency with few threads or streaming responses. If your dependencies are blocking JDBC, plain MVC is usually simpler and just as fast.

  • If your repository is blocking JDBC, does returning Mono/Flux from the controller make it non-blocking?
    No. Wrapping a blocking call in a Mono doesn't make it non-blocking — the blocking happens when it runs. You'd have to offload it to Schedulers.boundedElastic(), or use a truly reactive driver like R2DBC. A blocking driver behind WebFlux gives you the worst of both worlds.
  • What happens if a @GetMapping returns an empty Mono?
    The client gets a 200 OK with an empty body by default, not a 404. To signal 'not found' you must switchIfEmpty to an error/ResponseStatusException or map to ResponseEntity.notFound().

saying these in an interview costs you the question

  • Thinking WebFlux needs completely different annotations from MVC
  • Believing wrapping blocking code in Mono makes it non-blocking
  • Assuming an empty Mono automatically produces a 404
  • Calling .subscribe() manually inside the controller method

context

open as a page

How do you write a Spring WebFlux controller handler using Kotlin coroutines, and how do suspend functions and Flow<T> return types map onto the reactive model?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Mark the handler method suspend fun and return a plain value or a Flow<T> instead of Mono/Flux. Spring adapts a suspend function to a single-value response and a Flow to a streaming (many-value) response automatically.

open as a page

What is a HandlerFunction<ServerResponse> in Spring WebFlux, and how does it differ from an annotated @RestController method?

level: juniorimportance: must knowfreq 60%

basics

~10 s

A HandlerFunction is a function that takes a ServerRequest and returns a Mono<ServerResponse>. It's the functional way to write a WebFlux endpoint, instead of using annotations like @GetMapping on a controller method.

open as a page

How do you receive an uploaded file in a Spring WebFlux controller?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Bind the named multipart part with @RequestPart to a FilePart parameter. FilePart exposes filename(), headers(), and transferTo(path) to save the upload. Return a Mono/Flux so the handler stays non-blocking.

open as a page

What are RequestPredicates in Spring WebFlux functional endpoints, and how do they connect an incoming request to a HandlerFunction?

level: juniorimportance: must knowfreq 55%

basics

~10 s

A RequestPredicate is a condition that tests an incoming request (its method, path, headers). In RouterFunctions.route(predicate, handler), if the predicate matches, that HandlerFunction runs and returns the response.

open as a page

What is a RouterFunction in Spring WebFlux, and how do you define one route with RouterFunctions.route()?

level: juniorimportance: must knowfreq 55%

basics

~10 s

A RouterFunction is code (not annotations) that maps an incoming request to a handler. You build it with RouterFunctions.route(), e.g. route().GET("/hello", req -> ServerResponse.ok().bodyValue("hi")).build().

open as a page

How do awaitBody, awaitSingle, and related await* extensions bridge Reactor Mono/Flux into coroutine code, and what are their empty/multi-value semantics?

level: middleimportance: must knowfreq 45%

basics

~20 s

They are suspending extension functions (from kotlinx-coroutines-reactor) that subscribe to a Mono/Flux and suspend the coroutine until a value arrives, then return it. awaitSingle() expects exactly one value; awaitBody<T>() reads and deserializes a request/response body inside a suspend function.

open as a page

How do you extract the request body inside a HandlerFunction, and when do you use bodyToMono versus bodyToFlux?

level: middleimportance: must knowfreq 55%

basics

~10 s

Call request.bodyToMono(MyType.class) when the body is a single object, or request.bodyToFlux(MyType.class) when the body is a stream/array of many objects. Both return reactive publishers you then chain onto.

open as a page

How do you build a ServerResponse, and what is the difference between .body(...) and .bodyValue(...)?

level: middleimportance: must knowfreq 50%

basics

~10 s

Use the builder: ServerResponse.ok() (or .status(...), .created(uri), etc.), set headers, then a terminal call. Use bodyValue(obj) for a plain already-available object; use body(publisher, Class) when the body is a Mono/Flux.

open as a page

How do you compose RequestPredicates with and(), or(), and negate(), and what does each combinator mean semantically?

level: middleimportance: must knowfreq 60%

basics

~10 s

RequestPredicate has and(), or(), and negate(). and() requires both predicates to match, or() requires at least one, negate() inverts. You chain them to build precise route conditions like GET("/x").and(accept(APPLICATION_JSON)).

open as a page

How do you stream an uploaded part's content as DataBuffers to storage without buffering the whole file in memory?

level: seniorimportance: must knowfreq 40%

basics

~10 s

Use part.content(), which is a Flux<DataBuffer> streaming the body in chunks. Write it with DataBufferUtils.write(content, path) or FilePart.transferTo(path). Both stream chunk-by-chunk with backpressure and release each buffer.

open as a page

How do @PathVariable and @RequestParam behave in a WebFlux annotated controller — are they reactive, and how do you bind optional or multi-valued query params?

level: middleimportance: should knowfreq 45%

basics

~20 s

They work exactly like in MVC and are plain (non-reactive) values — the URI and query string are already available, so you bind String/int directly. Use required=false or Optional for optional params, and List<T> for multi-valued ones.

open as a page

What is the difference between declaring @RequestBody User user and @RequestBody Mono<User> user in a WebFlux handler?

level: middleimportance: should knowfreq 55%

basics

~20 s

@RequestBody User fully decodes the body before the method runs. @RequestBody Mono<User> hands you a publisher of the body — decoding is deferred until you subscribe, so you compose the request body into your reactive pipeline.

open as a page

What is the coRouter { } DSL in Spring WebFlux and how do coroutine-based functional handlers differ from the reactive router() DSL?

level: middleimportance: should knowfreq 35%

basics

~20 s

coRouter { } is the coroutine version of the functional routing DSL. Routes map to suspend handler functions that take a ServerRequest and return a ServerResponse directly (not a Mono<ServerResponse>), so you write sequential coroutine code.

open as a page

How do you handle a multipart request with an unknown number of parts (e.g. multiple files) in WebFlux?

level: middleimportance: should knowfreq 45%

basics

~10 s

Bind the whole body as Flux<Part> (or Mono<MultiValueMap<String, Part>>). Iterate the flux, check each part's type (FilePart vs FormFieldPart) via instanceof, and process filename/value accordingly.

open as a page

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

level: middleimportance: should knowfreq 45%

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.

open as a page

What do nest() and path() do in a RouterFunctions builder, and when would you use them?

level: middleimportance: should knowfreq 35%

basics

~10 s

nest() groups several routes under a shared RequestPredicate (like a common path prefix or Accept header) so you don't repeat it on every route. path("/x", ...) is shorthand for nest() with a path predicate.

open as a page

You return Flux<User> from a @GetMapping. How is it serialized, and how do you make the endpoint actually stream results to the client instead of sending one JSON array?

level: seniorimportance: should knowfreq 50%

basics

~10 s

By default Flux<User> is serialized as a single JSON array (application/json). To stream element-by-element, set produces to text/event-stream (SSE) or application/x-ndjson, so each item is flushed as it is emitted.

open as a page

Under the hood, how does Spring WebFlux adapt a suspend function or Flow<T> handler return value into Reactor Mono/Flux, and what role does kotlinx-coroutines-reactor play?

level: seniorimportance: should knowfreq 35%

basics

~20 s

WebFlux detects the suspend modifier/Flow return via reflection and uses kotlinx-coroutines-reactor to bridge them to Reactor: a suspend invocation is wrapped in a mono { } builder (→ Mono), and a Flow<T> is turned into a Flux<T> via asFlux(), registered through Reactor's ReactiveAdapterRegistry.

open as a page

How do you wire handler beans into routes with a RouterFunction, and how does the request reach the right handler method?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Define a @Bean RouterFunction<ServerResponse> using RouterFunctions.route(), mapping each path+method predicate to a handler method reference (e.g. .GET("/users/{id}", handler::getUser)). Spring uses the router to dispatch each request to the matching handler.

open as a page

Which HttpMessageReaders decode multipart in WebFlux, and how do MultipartHttpMessageReader, DefaultPartHttpMessageReader, and the PartEvent reader differ?

level: seniorimportance: should knowfreq 30%

basics

~10 s

DefaultPartHttpMessageReader parses the body into a streaming Flux<Part>. MultipartHttpMessageReader wraps it and collects parts into a Mono<MultiValueMap<String, Part>>. PartEventHttpMessageReader produces a fully streaming Flux<PartEvent> without buffering to disk.

open as a page

What is the difference between the accept() and contentType() RequestPredicates, and which HTTP headers do they match?

level: seniorimportance: should knowfreq 45%

basics

~20 s

accept() matches the request's Accept header (the media type the client wants back). contentType() matches the request's Content-Type header (the media type of the body the client is sending). They test different headers and are used for different purposes.

open as a page

How does the path()/GET() RequestPredicate match URLs, and what path-pattern syntax and matching-order pitfalls should you know?

level: seniorimportance: should knowfreq 40%

basics

~20 s

path()/GET() match the request path using a PathPattern. Syntax: {var} captures a path variable, * matches one segment, ** matches multiple. Because the first matching route wins, declare specific routes before broad ones so they aren't shadowed.

open as a page

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

level: seniorimportance: should knowfreq 40%

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.

open as a page

In an annotated WebFlux controller, who subscribes to the returned publisher, where is the threading danger, and how do you return proper HTTP status codes and handle errors reactively?

level: principalimportance: should knowfreq 40%

basics

~10 s

The framework subscribes to your Mono/Flux on the event-loop thread — so never block it. For status/errors, use ResponseEntity or ResponseStatusException, compose errors with onErrorResume/switchIfEmpty, and centralize with @ExceptionHandler; offload blocking work to boundedElastic.

open as a page

In a coroutine-based WebFlux endpoint, how are threading, cancellation, and blocking calls handled, and what must you get right to keep the event loop healthy?

level: principalimportance: should knowfreq 25%

basics

~20 s

Suspend handlers run on the non-blocking Reactor event-loop threads, so never call blocking code directly — offload it with withContext(Dispatchers.IO). Client disconnects/timeouts cancel the Reactor subscription, which cancels the coroutine (a CancellationException); write cancellation-cooperative code.

open as a page

In a functional WebFlux endpoint, how do you handle errors and empty results so the client gets the right status, and what threading pitfalls must you avoid?

level: principalimportance: should knowfreq 35%

basics

~10 s

Keep everything in the reactive chain: use switchIfEmpty to turn an empty Mono into a 404/400 response, and onErrorResume to map exceptions to error responses. Never block; always return the composed Mono<ServerResponse>.

open as a page

What memory, backpressure, and buffer-lifecycle pitfalls must you handle when consuming reactive multipart uploads, and how do you build a safe zero-buffer streaming pipeline?

level: principalimportance: should knowfreq 18%

basics

~10 s

Consume parts sequentially (concatMap), always release DataBuffers on every path including discard/cancel/error, set reader limits (maxInMemorySize/maxParts/maxDiskUsagePerPart), let backpressure throttle reads, and prefer PartEvent to relay uploads downstream without touching heap or disk.

open as a page

Explain how RequestPredicate matching works internally (state changes on ServerRequest, nested routing) and how you'd write a custom RequestPredicate.

level: principalimportance: nice to knowfreq 22%

basics

~20 s

A RequestPredicate implements test(ServerRequest). Matching can have side effects — it stores path variables and the matched pattern on the request, and nested()/subRoute() can mutate the request (e.g. consume a path prefix). Implement RequestPredicate and override test() (and nest()) for custom logic.

open as a page

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%

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.

open as a page