Shahul Basha
From the archive · 2020

Reactive Programming in Spring: Introduction

Why blocking RestTemplate calls limit scalability, how WebClient, Mono and Flux avoid it, what backpressure is, and when reactive isn't worth it.

  • 5 min read
  • Spring, Reactive, Java

Archive note I wrote this in 2020 on my old blog. It moved here in 2026 with the code and diagrams redone; library versions and APIs may have changed since. Original post.

Contents

In this article we’ll take a short look at reactive programming, and at why the Spring team put RestTemplate into maintenance mode in favour of WebClient, introduced in Spring 5.

Here’s how the Spring docs define reactive programming:

Reactive programming is about non-blocking applications that are asynchronous and event-driven and require a small number of threads to scale vertically rather than horizontally.

Blocking vs non-blocking

To understand non-blocking applications, let’s start with the traditional, blocking way of writing the code:

UserService.java (blocking)Java
@Service
public class UserService {

    @Autowired
    RestTemplate restTemplate;

    public UserResponseModel getUserDetailsBlocking(String id) {
        UserResponseModel userModel = restTemplate.getForObject(
                "http://localhost:8081/app/home/" + id, UserResponseModel.class);
        System.out.println(userModel.getName());
        return userModel;
    }
}

This calls http://localhost:8081/app/home/{id}, a service I made deliberately slow: it answers after 3 seconds. The application’s thread is blocked until the response comes back. That wastes hardware and limits scalability. Why?

The thread-per-request model: every thread waits, blocked, until the slow call returns.

A Spring Boot web application runs in a servlet container, which uses a thread per request: each incoming request gets a thread that sees it through to the end. The default pool has 200 threads. If they’re all calling a service that’s currently slow, they all sit in a WAITING state and new requests queue up. You can raise the pool from 200 to, say, 500, but every thread costs memory and CPU time for context switching. Horizontal scaling can help, up to a point.

Horizontal scaling

With horizontal scaling you run more instances of the application, for example as containers in Kubernetes, behind a load balancer. Each instance still has its default 200 threads, but five instances give you 1,000 in total.

Horizontal scaling adds capacity, but every instance still blocks on the same slow call.

But that doesn’t really solve the problem. If the application becomes popular, you keep adding instances, and paying for them in memory and CPU, all because a slow service is blocking your threads.

Non-blocking, asynchronous applications

WebClient arrived in Spring 5, as part of Spring WebFlux, as a non-blocking alternative to RestTemplate built for reactive programming. Here’s the same call written with it. First, a WebClient pointed at the service:

WebClient beanJava
@Bean
public WebClient webClient() {
    return WebClient.builder().baseUrl("http://localhost:8081/app").build();
}

Then the service:

UserService.java (non-blocking)Java
@Service
public class UserService {

    @Autowired
    WebClient client;

    public Mono<UserResponseModel> getUserDetails(String id) {
        System.out.println("ENTERS NON-BLOCKING METHOD");
        Mono<UserResponseModel> monoUser = this.client.get().uri("/home/{id}", id)
                .retrieve()
                .bodyToMono(UserResponseModel.class);

        // Prints the user name "Shahul" once the response is received
        monoUser.subscribe(user -> {
            System.out.println(user.getName());
        });

        System.out.println("NON-BLOCKING METHOD COMPLETED");

        // Returns the response at a later point
        return monoUser;
    }
}

It calls the same slow URL, http://localhost:8081/app/home/{id}. The output is:

Output
ENTERS NON-BLOCKING METHOD
NON-BLOCKING METHOD COMPLETED
"Shahul"

The method finishes before the name is printed: nothing waits for the slow service. Let’s look at the pieces involved.

Mono

Mono comes from Project Reactor, the reactive library Spring WebFlux is built on. It represents a value that will arrive at some point in the future, and has methods for working with that value when it does. You can wrap a response object in a Mono.

Flux

Flux is like Mono, but for many values. Use Mono for a single object and Flux when the response is a sequence of values. I haven’t used Flux here, to keep things simple.

The publisher–subscriber model

Mono is the publisher. When the data arrives from the slow service, it’s pushed to its subscribers. In the example I subscribe explicitly to print the name. When a browser calls an endpoint that returns a Mono, the framework subscribes on its behalf and streams the result to the browser as soon as it’s available.

The Reactive Streams specification defines this as a sequence of events. In our example:

The Reactive Streams handshake. The thread is free between request(n) and onNext.

Earlier I said reactive web applications are highly scalable and use resources efficiently. Here’s why:

A reactive server: threads hand off I/O and move on, so they never sit waiting.

In a reactive application every activity is an event, and the whole application is event-driven. As the diagram shows, a thread never sits waiting. The obvious benefit: far fewer threads.

The CPU doesn’t have to switch between lots of threads, and the thread pool can stay as small as the application needs, which takes load off the CPU.

Another important advantage of this model is backpressure.

Backpressure

Imagine a client that asks for every matching item in a database so it can process them. The backend has 1,000 results and is ready to send the whole list.

The client can’t process that much at once, so it asks for 50 at a time. That’s backpressure: the subscriber uses request(n) to tell the publisher how much it can handle. Because data arrives one event at a time, the publisher can respect that limit.

Does that mean everything should be reactive from now on? No. Reactive programming helps in some cases and not in others.

Disadvantages

Processing data is harder

Any processing on a Mono or Flux has to use the Reactor API’s operators. Everything is event-driven and based on streams of data, which is quite different from imperative code.

It has to be reactive end to end

The whole stack has to be reactive, including database drivers, so the database can notify the publisher when its data is ready, and every API call. A single blocking call anywhere defeats the purpose.

Debugging is harder

Stack traces are less helpful, and logs need care, because everything happens as events on shared threads.

It suits microservices more than monoliths

Reactive programming pays off mostly in microservices and distributed systems. In a monolith it’s usually less useful.

Conclusion

We’ve barely scratched the surface of reactive programming, but this should give you a solid footing for going further.

The projects are on GitHub: shahulbasha/reactiveapplication.

References

← All writing