Somewhere along the way, reactive became the "serious" choice. New Spring services default to WebFlux because it sounds more scalable. Most of them shouldn't. Reactive programming solves a real problem, but it is not the problem most teams have.
What WebFlux Buys You
WebFlux lets a handful of threads serve enormous numbers of concurrent connections by never blocking. For a gateway fronting thousands of slow, long-lived streams, that is genuinely valuable. If you are building an API gateway or a service that fans out to dozens of downstream calls per request, reactive earns its keep.
What It Costs You
Everything else gets harder:
// Imperative — anyone can read this public Order load(Long id) { Order order = repo.findById(id).orElseThrow(); order.setLines(lineRepo.findByOrder(id)); return order; } // Reactive — now everything is a Mono/Flux, forever public Mono<Order> load(Long id) { return repo.findById(id) .switchIfEmpty(Mono.error(new NotFound())) .flatMap(order -> lineRepo.findByOrder(id) .collectList() .map(lines -> { order.setLines(lines); return order; })); }
Stack traces become useless. Debugging is painful. Every developer must think in operators. A single blocking call sneaked into the chain quietly ruins the whole benefit. And your entire stack — driver, database, libraries — must be reactive end to end, or it collapses to blocking anyway.
Virtual Threads Changed the Calculus
The main historical reason to accept that pain was thread efficiency: you couldn't afford one blocking thread per request at high concurrency. Virtual threads removed that constraint. You can now write plain, imperative, blocking code and still handle massive concurrency. The chief argument for WebFlux in ordinary CRUD services has largely evaporated.
Where I Land
Reach for WebFlux when you have a genuinely streaming, high-fan-out, backpressure-sensitive workload and a fully reactive stack to match. For the standard request/response service — which is most services — write imperative code on virtual threads. It is easier to read, easier to debug, and now scales just fine.
Default to simple. Make reactive a deliberate choice you can justify, not the reflex.