Reactive Spring: WebFlux, Mono and Flux
Reactive programming handles data as asynchronous streams and never blocks a thread while waiting for I/O.
- Reactive Streams defines Publisher, Subscriber and Subscription (also in the JDK as java.util.concurrent.Flow). Subscribers request items with request(n): that's backpressure.
- Project Reactor provides Mono (0 or 1 item) and Flux (0 to many), with operators such as map, filter, flatMap and zip.
- Nothing happens until something subscribes. Building a chain only describes the work.
- Spring WebFlux is the reactive web framework: controllers return Mono or Flux, it runs on a small number of event-loop threads (Netty), and WebClient is its non-blocking HTTP client. R2DBC provides reactive database access.
When to use it: very high concurrency, streaming data (server-sent events), or gateways that mostly wait on other services. Since Java 21, virtual threads let ordinary blocking Spring MVC code scale to similar concurrency with simpler code, so for typical CRUD services Spring MVC remains the default choice.
Example
@RestController
@RequestMapping("/api/courses")
class CourseController {
private final CourseRepository repo; // a ReactiveCrudRepository (R2DBC)
private final WebClient reviews;
CourseController(CourseRepository repo, WebClient.Builder builder) {
this.repo = repo;
this.reviews = builder.baseUrl("https://reviews.internal").build();
}
@GetMapping
Flux<Course> all() {
return repo.findAll().filter(Course::published); // streamed, not loaded all at once
}
@GetMapping("/{id}")
Mono<CourseView> one(@PathVariable long id) {
Mono<Course> course = repo.findById(id);
Mono<Double> rating = reviews.get().uri("/ratings/{id}", id)
.retrieve().bodyToMono(Double.class)
.onErrorReturn(0.0); // degrade gracefully
return Mono.zip(course, rating, CourseView::new); // both calls run concurrently
}
}Common mistake
Mixing blocking calls (JDBC, RestTemplate, block()) into WebFlux code. It throws away the benefit and can freeze the server under load.
Under the hood
Never block inside a reactive pipeline: calling block(), a JDBC driver or Thread.sleep on an event-loop thread stalls every request that thread serves. Debugging is harder because stack traces show the framework, not your call chain (Reactor's checkpoint() and Hooks help). Reactive code pays off when the whole path is non-blocking, from the controller through the HTTP client to the database driver.
Check yourself
What happens when you build a Flux chain but nobody subscribes?
How this connects
Know these first
Where this leads
You've reached the end of this thread. Try a learning path for what's next.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.