How to Implement Reactive Programming in Java with Spring WebFlux

If you have built a Spring Boot application before, you know the pain of blocking threads piling up under load. Every request grabs a thread from the pool, holds it until the response arrives, and then releases it. That works fine when traffic is low, but as your system scales, thread management becomes a bottleneck. Reactive programming flips that model. Instead of waiting, your code reacts to events. Spring WebFlux makes this approach feel natural inside the familiar Spring ecosystem. You keep your annotations, your dependency injection, and your familiar structure, but your services become non-blocking and resilient.

Key Takeaway

Spring WebFlux allows Java developers to build reactive, non-blocking applications with the same declarative style they already know. By switching from Servlet-based Tomcat to Netty, and from blocking data access to R2DBC or reactive MongoDB drivers, you can handle many more concurrent connections with fewer resources. This guide walks you through the full setup, from a basic reactive endpoint to testing and error handling.

Why Go Reactive with Spring WebFlux

Traditional Spring MVC uses a thread per request model. When you call a database or an external API, that thread sleeps until the result comes back. With reactive programming, your code publishes a stream of data and continues processing other work. Project Reactor, the library underlying WebFlux, provides Mono (for 0 or 1 item) and Flux (for 0 to N items). These types let you compose asynchronous operations without blocking.

The biggest win is resource efficiency. A single Netty event loop can handle thousands of concurrent connections. Your application can serve more users on the same hardware. That is especially valuable for microservices that handle long lived connections, streaming data, or real time updates.

Setting Up a Spring WebFlux Project

Start from Spring Initializr. Choose Spring WebFlux instead of Spring Web. Add the reactive dependencies you need. For a basic REST API, just include Spring Reactive Web. If you plan to connect to a database, add R2DBC or Spring Data Reactive MongoDB.

Your main class stays identical to any Spring Boot app. The difference is under the hood. Instead of Tomcat, the starter pulls in Netty. You can verify by running the app and looking at the logs. You will see “Netty started on port 8080”.

Building Your First Reactive Endpoint

Create a controller the same way you would with MVC. Annotate it with @RestController. The key change: return types become Mono or Flux.

@RestController
@RequestMapping("/tasks")
public class TaskController {

    @GetMapping("/{id}")
    public Mono<Task> getTask(@PathVariable String id) {
        return taskService.findById(id);
    }

    @GetMapping
    public Flux<Task> getAllTasks() {
        return taskService.findAll();
    }
}

No CompletableFuture, no Callable. Just Mono and Flux. Spring WebFlux knows how to subscribe to them and stream the response.

Connecting to a Database with R2DBC

Blocking JDBC kills the whole point of reactive. Use R2DBC (Reactive Relational Database Connectivity) to talk to SQL databases without blocking. Add spring-boot-starter-data-r2dbc and a driver for your database (PostgreSQL, MySQL, H2).

Define a repository just like Spring Data JPA, but extending ReactiveCrudRepository. Your queries return Mono or Flux.

public interface TaskRepository extends ReactiveCrudRepository<Task, String> {
    Flux<Task> findByStatus(String status);
}

Then inject the repository into your service. All operations are reactive. No thread waits for the database.

Error Handling in Reactive Streams

Errors in reactive streams are handled differently than in imperative code. You cannot use try-catch around a Mono. Instead, you chain operators like onErrorReturn, onErrorResume, or doOnError.

public Mono<Task> findById(String id) {
    return repository.findById(id)
        .switchIfEmpty(Mono.error(new TaskNotFoundException(id)))
        .onErrorResume(e -> {
            log.error("Error fetching task", e);
            return Mono.error(e);
        });
}

Spring WebFlux also supports @ExceptionHandler in controllers for returning proper HTTP status codes. The pattern stays the same, but the execution is non-blocking.

Testing Reactive Endpoints

Testing reactive code requires a slightly different mindset. Use StepVerifier from Project Reactor to assert on streams.

@Test
void testGetAllTasks() {
    Flux<Task> tasks = taskController.getAllTasks();
    StepVerifier.create(tasks)
        .expectNextMatches(task -> task.getName().startsWith("T"))
        .expectNextCount(2)
        .verifyComplete();
}

For integration tests, use WebTestClient instead of TestRestTemplate. It understands reactive types and can perform assertions without blocking.

webTestClient.get().uri("/tasks")
    .exchange()
    .expectStatus().isOk()
    .expectBodyList(Task.class).hasSize(3);

Common Mistakes and How to Avoid Them

Transitioning from MVC to WebFlux often leads to subtle bugs. The table below shows typical pitfalls and the correct reactive approach.

Mistake Why It Hurts Fix
Calling .block() inside a Mono stream Blocks the event loop, defeating concurrency Use flatMap or compose rather than blocking
Using Thread.sleep() in a reactive pipeline Stops the entire thread, causing latency Use Mono.delay() for timed operations
Sharing mutable state across operators Race conditions and unpredictable output Each subscriber gets its own stream; use flatMap and avoid shared variables
Ignoring backpressure Downstream can be overwhelmed by fast producers Apply operators like limitRate() or onBackpressureBuffer()
Forgetting to subscribe No execution happens Rely on Spring WebFlux to subscribe in controllers, but in custom code call .subscribe()

“Reactive programming is not about making your code async. It is about changing the way you think about data flows. Start with small services and let the reactive mindset grow naturally. Do not rewrite your entire monolith at once.”
— [Expert advice from a Spring team member at SpringOne 2025]

Step by Step: Migrating a Simple Service from MVC to WebFlux

If you have an existing Spring MVC service and want to try reactive, follow these steps:

  1. Swap the starter: Replace spring-boot-starter-web with spring-boot-starter-webflux. Remove any Tomcat specific configuration.
  2. Change return types: Update controller methods to return Mono<T> or Flux<T>. Wrap single results with Mono.just() and collections with Flux.fromIterable().
  3. Replace blocking calls: If your service calls a blocking database or HTTP client, swap to reactive equivalents. Use WebClient instead of RestTemplate.
  4. Refactor service layer: Move logic that depends on sequential calls into flatMap or zip operators. Avoid CompletableFuture in favor of Mono.
  5. Test with StepVerifier: Rewrite existing unit tests to use StepVerifier and integration tests to use WebTestClient.

Remember that not every project needs 100% reactive. A hybrid architecture where some services are reactive and some remain blocking is perfectly valid. Start with the parts that handle high concurrency or streaming.

Benefits You Can Expect

  • Lower memory footprint per request. Netty event loops use a handful of threads.
  • Higher throughput under load. More connections per second with the same hardware.
  • Simpler code for streaming responses. Sending server sent events or chunked data becomes straightforward.
  • Improved resilience. Reactive streams handle backpressure natively, protecting downstream services.

Wrapping Up Your Reactive Journey

Spring WebFlux is not a magic switch. It requires a shift in how you think about data and control flow. But once you get comfortable with Mono and Flux, building scalable, non-blocking services becomes second nature. Start small: pick one endpoint that faces high traffic and convert it. Use WebClient for external calls. Test with StepVerifier. Over time, the reactive approach will feel as natural as the blocking one you already know.

If you enjoyed this guide, you might also find our articles on mastering asynchronous programming in JavaScript and the top open source frameworks of 2026 useful for broadening your async skills across languages.

Leave a Reply

Your email address will not be published. Required fields are marked *