If your Java application struggles under load, with threads blocked waiting for databases or external APIs, you know the pain. A Tomcat pool of 200 threads isn't enough when each request holds one for seconds. At Meteora Web, we've seen too many backends choke because they were written with the synchronous template. Reactive programming with Project Reactor and Spring WebFlux isn't a trend—it's how you squeeze every drop of performance from your hardware without wasting a thread while waiting for a response.
What is reactive programming and why does your Java app need it?
Reactive programming is an asynchronous paradigm based on data streams and event propagation. Instead of calling a method and waiting for its return, you subscribe to a stream and react whenever data arrives. Project Reactor is the reference implementation for the JVM and the engine behind Spring WebFlux.
Why do you need it? Modern applications spend most of their time on I/O (databases, REST calls, file system). With the blocking model, each I/O operation holds a thread. With the reactive model, the thread is freed and can handle other requests while the operation is in progress. Result: fewer threads, higher concurrency, lower latency.
Concrete example: a database call that takes 200 ms. With Tomcat and a pool of 10 threads, you can serve at most 50 requests per second. With WebFlux and a single event-loop thread, you can serve hundreds because while waiting for the database, the thread handles other requests.
Sponsored Protocol
Blocking synchronous vs asynchronous reactive
// Blocking (Spring MVC)
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow(); // thread blocked until result arrives
}
// Reactive (Spring WebFlux)
@GetMapping("/user/{id}")
public Mono getUser(@PathVariable Long id) {
return userRepository.findById(id); // no blocking, thread returns to loop
}
What are the fundamental types of Project Reactor: Mono and Flux?
Project Reactor has two main types: Mono for 0 or 1 element, Flux for 0 or N elements. Both implement the Publisher interface and comply with Reactive Streams (backpressure, subscriber, subscription).
Creating a stream is straightforward:
Mono hello = Mono.just("Hello Meteora Web");
Flux numbers = Flux.just(1, 2, 3, 4, 5);
Essential operators
Operators let you transform, filter, and combine streams. Some fundamental ones:
- map: synchronous transformation of each element
- flatMap: transforms each element into a Publisher and flattens (for async calls)
- filter: removes elements that don't satisfy a predicate
- zip: combines two or more streams into one
Flux names = Flux.just("anna", "marco", "luigi");
Flux uppercase = names.map(String::toUpperCase); // "ANNA", "MARCO", "LUIGI"
Flux users = names.flatMap(name -> userRepository.findByName(name)); // async calls
Error handling
Reactor provides operators to handle errors without breaking the chain:
Sponsored Protocol
Flux withError = Flux.just(1, 2, 0)
.map(i -> 10 / i)
.onErrorReturn(-1); // if division by zero, returns -1 and continues
// Or retry
Mono withRetry = externalCall()
.retry(3); // retry up to 3 times on error
How to configure Spring WebFlux for a reactive endpoint?
To get started with Spring WebFlux, add the Maven dependency:
org.springframework.boot
spring-boot-starter-webflux
Spring Boot automatically configures Netty as the embedded server (instead of Tomcat). Writing a reactive controller is similar to Spring MVC, but methods return Mono or Flux:
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@GetMapping
public Flux getAllBooks() {
return bookRepository.findAll(); // reactive repository (R2DBC or MongoDB reactive)
}
@GetMapping("/{id}")
public Mono> getBook(@PathVariable String id) {
return bookRepository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}
Note: the repository must be reactive. For relational databases use R2DBC, for MongoDB the native reactive driver. Spring Data Reactive supports both.
Sponsored Protocol
WebClient: the reactive replacement for RestTemplate
For external HTTP calls, ditch RestTemplate (deprecated) in favor of WebClient, which is reactive:
WebClient webClient = WebClient.create("https://api.example.com");
Mono response = webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(JsonNode.class);
How to handle backpressure and threading in a reactive stream?
Backpressure is the mechanism by which the consumer signals the producer how many elements it can handle. If the producer is faster than the consumer, without backpressure memory will saturate. Reactor handles backpressure automatically, but you can control it with operators like limitRate() or onBackpressureDrop().
Sponsored Protocol
Flux.range(1, 1_000_000)
.limitRate(100) // requests 100 items at a time
.subscribe(System.out::println);
Schedulers: where to run heavy work
For blocking operations (e.g., file write, calling non-reactive libraries) use Schedulers.boundedElastic(). For CPU-intensive work use Schedulers.parallel(). By default Reactor runs everything on the subscription thread (event-loop).
Mono.fromCallable(() -> {
// blocking code
return fileService.writeToDisk();
})
.subscribeOn(Schedulers.boundedElastic()) // runs on a dedicated thread
.subscribe();
What common mistakes to avoid when starting with WebFlux?
Developers coming from Spring MVC tend to repeat the same errors. Here are the most frequent:
- Calling .block() inside a reactive stream: blocks the event-loop thread, nullifying any benefit. Never use
block()in a reactive context unless absolutely necessary (e.g., in a test). - Mixing blocking code without Schedulers: if you have to write to a file, do it on
boundedElastic, not directly in a Reactor stream. - Ignoring backpressure: if your producer generates millions of elements, make sure the subscriber is ready or use control operators.
- Using RestTemplate in a WebFlux application:
RestTemplateis blocking; useWebClientinstead.
Example of an error:
Sponsored Protocol
// WRONG: block() in controller
@GetMapping("/user/{id}")
public User getUserBlocking(@PathVariable Long id) {
return userRepository.findById(id).block(); // blocks the event-loop thread!
}
// CORRECT
@GetMapping("/user/{id}")
public Mono getUserCorrect(@PathVariable Long id) {
return userRepository.findById(id);
}
What to do next
We've covered the fundamentals. Now it's time for action:
- Create a Spring Boot project with webflux and write an endpoint that returns
Mono.just("Hello Reactive World"). - Try WebClient by calling a public API (e.g., JSONPlaceholder) and return the result as a
Flux. - Read the official documentation of Project Reactor to dive deeper into operators like
flatMapSequential,delayElements,timeout. - If you use relational databases, add R2DBC and Spring Data Reactive for a fully reactive stack.
At Meteora Web, we use these technologies in projects that demand high performance and scalability. If you want to see how to apply them to your stack, check out our guide on modern Java and Spring Boot 3.