Skip to main content

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.

Pattern-check challenges

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
Starters

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
Quiz

Reactive basics

1 / 2

Backpressure 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).

TypeEmits
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:

ModelClient → Server
Fire-and-forget1 message, no reply
Request-response1 → 1
Request-stream1 → stream
Channelstream ↔ 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;
}
}
Quiz

RSocket quiz

1 / 2

Fire-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​

TopicIdea
ReactiveAsync streams, subscribe, backpressure
ReactorFlux (0..N), Mono (0..1)
WebFlux MVC-styleSame annotations; return reactive types
FunctionalRouterFunction + HandlerFunction
WebClientNon-blocking HTTP client
RSocketFour interaction models over duplex protocol
WebSocketBidirectional; often STOMP destinations

Full samples: ch08 on GitHub.

Next in the book: deploying Boot apps → Lesson 7.

Quiz

Chapter wrap-up

1 / 3

ReactiveMongoRepository methods typically return…

Pick an answer to see if you are right.