Reactive Spring Boot with WebFlux
Source: Spring Boot in Practice — Chapter 8 by Somnath Musib (Manning). Concepts rewritten for this course; companion samples: spring-boot-in-practice/repo ch08.
Non-blocking APIs with Spring WebFlux and Project Reactor, then WebClient, RSocket, and WebSocket for richer client–server communication.
Editors do not compile or run Spring Boot. Check uses sourceChecks only. Snippets above exercises show the usual shape with inline comments; type the code yourself. Paste is disabled on Spring Boot lessons.
What this lesson covers
- Reactive ideas: async streams, backpressure,
Flux/Mono - WebFlux annotated controllers and functional endpoints
- Calling APIs with
WebClient - RSocket interaction models
- WebSocket + STOMP messaging
Use spring-boot-starter-webflux (not starter-web) for a reactive stack. Mixing both is possible but easy to get wrong — prefer one stack per app while learning.
1. Reactive programming in brief
A data stream emits values over time (clicks, DB rows, messages). In reactive code you subscribe to a stream and react to:
- next — a value
- error — failure
- complete — stream finished
Asynchronous means the calling thread does not block waiting; work continues and the result arrives later.
Backpressure: if a producer is faster than a consumer, unbounded buffers risk OOM. Reactive Streams let the subscriber request(n) — pull what it can handle.
Benefits vs blocking + nested callbacks/Future hell:
- Non-blocking I/O (better resource use under load)
- Declarative operators (
map,flatMap,filter, …) - Backpressure built into the model
- Nothing runs until
subscribe()(cold publishers)
Flux<Integer> ints = Flux.just(1, 2, 3);
ints.map(i -> i * 2).subscribe(System.out::println); // subscribe starts the pipeline
Reactive basics
1 / 2Backpressure primarily lets a consumer…
Pick an answer to see if you are right.
2. Project Reactor: Flux and Mono
Reactor implements Reactive Streams (Publisher, Subscriber, Subscription, Processor).
| Type | Emits |
|---|---|
Flux<T> | 0..N items, then complete or error |
Mono<T> | 0..1 item, then complete or error |
Flux<Integer> a = Flux.just(1, 2, 3);
Flux<Integer> b = Flux.range(1, 10);
Flux<String> c = Flux.fromIterable(List.of("foo", "bar"));
Mono<Integer> empty = Mono.empty();
Mono<Integer> one = Mono.just(1);
Flux<Integer> intFlux = Flux.just(1, 2, 3);
// map each element, then subscribe to print
3. WebFlux annotated controllers
WebFlux offers two styles: annotated controllers (familiar @GetMapping) and functional endpoints (RouterFunction).
Typical reactive Course Tracker stack:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
Repository returns reactive types:
public interface CourseRepository extends ReactiveMongoRepository<Course, String> {
Flux<Course> findAllByCategory(String category); // Flux = many courses
}
Controller returns Flux / Mono (and often Mono<ResponseEntity<…>> for 404):
@RestController
@RequestMapping("/courses/")
public class CourseController {
private final CourseRepository courseRepository;
@GetMapping
public Flux<Course> getAllCourses() {
return courseRepository.findAll(); // 0..N
}
@GetMapping("{id}")
public Mono<ResponseEntity<Course>> getCourseById(@PathVariable String id) {
return courseRepository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build()); // 404 when empty
}
@PostMapping
public Mono<Course> createCourse(@RequestBody Course course) {
return courseRepository.save(course);
}
}
Nothing hits the DB until the HTTP layer subscribes to your returned publisher.
Seed data with a lazy chain that ends in subscribe:
Flux.just(course1, course2, course3)
.flatMap(courseRepository::save)
.thenMany(courseRepository.findAll())
.subscribe(System.out::println);
@RestController
@RequestMapping("/courses/")
public class CourseController {
private final CourseRepository courseRepository;
public CourseController(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
public Flux<Course> getAllCourses() {
return null;
}
public Mono<ResponseEntity<Course>> getCourseById(@PathVariable("id") String courseId) {
return null;
}
}
4. Functional endpoints
Instead of annotations, declare routes and handlers:
@Configuration
public class RouterContext {
@Bean
RouterFunction<ServerResponse> routes(CourseHandler handler) {
return route(GET("/courses").and(accept(APPLICATION_JSON)), handler::findAllCourses)
.andRoute(GET("/courses/{id}").and(accept(APPLICATION_JSON)), handler::findCourseById)
.andRoute(POST("/courses").and(accept(APPLICATION_JSON)), handler::createCourse)
.andRoute(DELETE("/courses/{id}").and(accept(APPLICATION_JSON)), handler::deleteCourse);
}
}
Handler takes ServerRequest, returns Mono<ServerResponse>:
public Mono<ServerResponse> findAllCourses(ServerRequest request) {
Flux<Course> courses = courseRepository.findAll();
return ServerResponse.ok().contentType(APPLICATION_JSON).body(courses, Course.class);
}
public Mono<ServerResponse> findCourseById(ServerRequest request) {
String id = request.pathVariable("id");
return courseRepository.findById(id)
.flatMap(course -> ServerResponse.ok().contentType(APPLICATION_JSON).bodyValue(course))
.switchIfEmpty(ServerResponse.notFound().build());
}
Routes are matched in order — put specific paths before catch-alls.
@Configuration
public class RouterContext {
@Bean
RouterFunction<ServerResponse> routes(CourseHandler courseHandler) {
// GET /courses → findAllCourses
// GET /courses/{id} → findCourseById
return null;
}
}
5. WebClient
WebClient is the reactive HTTP client (fluent, non-blocking). Prefer it over legacy RestTemplate for WebFlux apps.
WebClient client = WebClient.builder().baseUrl("http://localhost:8080/courses/").build();
public Mono<Course> getCourseById(String id) {
return client.get()
.uri("{id}", id)
.retrieve()
.bodyToMono(Course.class);
}
public Flux<Course> getAllCourses() {
return client.get()
.retrieve()
.bodyToFlux(Course.class);
}
public Mono<ResponseEntity<Course>> postNewCourse(Course course) {
return client.post()
.body(Mono.just(course), Course.class)
.retrieve()
.toEntity(Course.class);
}
Chain calls with operators (thenMany, flatMap, …) and still end with subscribe (or return the publisher to the framework).
@Component
public class WebClientApi {
private final WebClient webClient;
public WebClientApi() {
this.webClient = WebClient.builder().baseUrl("http://localhost:8080/courses/").build();
}
public Mono<Course> getCourseById(String id) {
return null; // GET {id} → bodyToMono(Course.class)
}
}
6. RSocket
RSocket is a multiplexed, duplex binary protocol (often over TCP or WebSocket) with four interaction models:
| Model | Client → Server |
|---|---|
| Fire-and-forget | 1 message, no reply |
| Request-response | 1 → 1 |
| Request-stream | 1 → stream |
| Channel | stream ↔ stream |
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-rsocket</artifactId>
</dependency>
spring.rsocket.server.port=7000
Map routes with @MessageMapping (messaging, not MVC path mapping):
@Controller
public class CourseController {
@MessageMapping("request-response")
public Mono<Course> requestResponse(Course course) {
return Mono.just(new Course("Your course name: " + course.getCourseName()));
}
@MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(Course course) {
return Mono.empty(); // no payload back
}
@MessageMapping("request-stream")
public Flux<Course> requestStream(Course course) {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> new Course("Your course name: " + course.getCourseName() + ". Response #" + i));
}
}
Tests often use RSocketRequester + Reactor StepVerifier.
@Controller
public class CourseController {
@MessageMapping("request-response")
public Mono<Course> requestResponse(final Course course) {
return null;
}
@MessageMapping("fire-and-forget")
public Mono<Void> fireAndForget(final Course course) {
return null;
}
}
RSocket quiz
1 / 2Fire-and-forget means…
Pick an answer to see if you are right.
7. WebSocket and STOMP
HTTP is request/response initiated by the client. WebSocket upgrades an HTTP connection to a full-duplex TCP channel so either side can push.
Raw WebSocket frames are low-level; Spring apps often use STOMP over WebSocket for destinations and routing.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS(); // handshake + SockJS fallback
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic"); // broker destinations
registry.setApplicationDestinationPrefixes("/app"); // @MessageMapping prefix
}
}
@Controller
public class MessageController {
@MessageMapping("/chat") // client sends to /app/chat
@SendTo("/topic/messages") // broadcast to subscribers
public OutputMessage message(InputMessage message) {
return OutputMessage.builder()
.time(Instant.now())
.content(message.getContent())
.build();
}
}
Browser clients often use SockJS + stomp.js: connect to /ws, send to /app/chat, subscribe to /topic/messages. Handshake uses Upgrade: websocket; success is 101 Switching Protocols.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// register /ws with SockJS
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// simple broker /topic + app prefix /app
}
}
Chapter checklist
| Topic | Idea |
|---|---|
| Reactive | Async streams, subscribe, backpressure |
| Reactor | Flux (0..N), Mono (0..1) |
| WebFlux MVC-style | Same annotations; return reactive types |
| Functional | RouterFunction + HandlerFunction |
| WebClient | Non-blocking HTTP client |
| RSocket | Four interaction models over duplex protocol |
| WebSocket | Bidirectional; often STOMP destinations |
Full samples: ch08 on GitHub.
Next in the book: deploying Boot apps → Lesson 7.
Chapter wrap-up
1 / 3ReactiveMongoRepository methods typically return…
Pick an answer to see if you are right.