PrepZone Logo
PrepZone

When Reactive Wins (and When It Does Not)

WebFlux vs MVC, Project Reactor basics and the virtual-threads alternative.

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 Mono and Flux.
HTTP request
DispatcherServlet
Controller
Service
JSON response
From HTTP arrival to JSON response — know where validation, security and exception handling sit.

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)

Java
@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)

Java
@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

Java
@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));
    }
}
Java
# 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.