Why this matters
- Choosing WebFlux for a CRUD BookStore API adds Project Reactor complexity without benefit if every endpoint blocks on JPA anyway.
- WebFlux shines when you proxy slow upstream APIs, stream large result sets, or compose many I/O calls without blocking threads.
- Virtual threads on Java 21+ give most MVC apps reactive-scale concurrency without rewriting controllers to return
MonoandFlux.
HTTP request
DispatcherServlet
Controller
Service
JSON response
MVC vs WebFlux decision factors
- Thread model — MVC: one platform (or virtual) thread per request. WebFlux: event loop with non-blocking I/O.
- Return types — MVC: plain objects,
ResponseEntity. WebFlux:Mono<T>,Flux<T>. - Database access — JPA is blocking; WebFlux needs R2DBC or offloads blocking calls to a separate pool.
- Ecosystem fit — MVC has mature Spring Data JPA, security, and validation. WebFlux requires reactive drivers throughout.
- Backpressure — WebFlux supports flow control for streaming; MVC buffers the full response.
Typical MVC controller (blocking, simple)
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/{isbn}")
public BookDetail getBook(@PathVariable String isbn) {
return bookService.getByIsbn(isbn);
}
@GetMapping
public List<BookSummary> search(@RequestParam String query) {
return bookService.search(query);
}
}
WebFlux controller (non-blocking)
@RestController
@RequestMapping("/api/books")
public class ReactiveBookController {
private final ReactiveBookRepository bookRepository;
public ReactiveBookController(ReactiveBookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@GetMapping("/{isbn}")
public Mono<BookDetail> getBook(@PathVariable String isbn) {
return bookRepository.findByIsbn(isbn)
.map(BookDetail::from)
.switchIfEmpty(Mono.error(new BookNotFoundException(isbn)));
}
@GetMapping(value = "/stream", produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<BookSummary> streamCatalog() {
return bookRepository.findAll()
.map(BookSummary::from);
}
}
Composing non-blocking upstream calls
@Service
public class BookEnrichmentService {
private final WebClient reviewsClient;
private final WebClient pricingClient;
public Mono<EnrichedBook> enrich(Book book) {
Mono<ReviewSummary> reviews = reviewsClient.get()
.uri("/reviews/{isbn}", book.getIsbn())
.retrieve()
.bodyToMono(ReviewSummary.class);
Mono<PriceInfo> pricing = pricingClient.get()
.uri("/prices/{isbn}", book.getIsbn())
.retrieve()
.bodyToMono(PriceInfo.class);
return Mono.zip(reviews, pricing,
(r, p) -> new EnrichedBook(book, r, p));
}
}
# WebFlux uses Netty by default instead of Tomcat
spring:
main:
web-application-type: reactive
Quick recall
Everything you need if you only revisit this box.
- MVC: blocking, thread-per-request, works naturally with JPA and most Spring ecosystem.
- WebFlux: non-blocking,
Mono/Flux, needs R2DBC or reactive clients throughout the stack. - Do not adopt WebFlux just for performance — virtual threads on MVC cover most I/O-bound REST APIs.
- WebFlux wins for streaming large datasets and composing multiple non-blocking upstream calls.
- Blocking calls on the Netty event loop destroy WebFlux performance — keep the stack fully reactive or use MVC.
Test yourself
Answer these before moving on — recall is what makes it stick.