Skip to content

Full Reactive Stack with Spring Boot, WebFlux and MongoDB

Building the backend of a reactive application with Spring Boot, WebFlux and MongoDB

This post is a blog version of one part of the Full Reactive Stack with Spring Boot, WebFlux, Angular, RxJS, and Eventsource Guide. The complete code sources are on GitHub.

This guide has four main chapters and an appendix:

  1. The Reactive Web. This chapter guides you through the main concepts of a reactive approach and compares it to the classic web interfaces.

  2. The backend. This section covers the creation of the Spring Boot application using the required dependencies and the development of the backend layers, paying special attention to the Controllers with WebFlux and the MongoDB reactive driver.

  3. The frontend. It goes through the development of an Angular application that consumes the reactive endpoint created on the server’s side.

  4. Conclusions. This part completes the guide, summarizing what you’ve built and comparing the Reactive and Classic Web approaches. It gives some insights on performance, testability, etc.

  5. Run the app with Docker. This appendix helps you build and run the full-stack application in Docker.

All the source code (spring-boot, Angular, Docker) is available on GitHub: Full-Reactive Stack repository. If you find it useful, please give it a star!

Within this chapter, you’ll learn how to develop the Reactive Web Server. We’ll use Spring WebFlux, which has been available since Spring 5 and is included in the Spring Boot 2.0 version. We’ll connect to a Mongo database using its reactive driver with Spring Data, also via Spring Boot.

We’ll include two different web approaches within the same backend application:

  1. A reactive style, using WebFlux and a MongoDB ReactiveCrudRepository.

  2. The classical Spring Web MVC stack uses a blocking REST controller and a blocking MongoDB query.

Additionally, we’ll use:

  • Docker to simplify MongoDB deployment.

  • A script to pre-load data into MongoDB. For that, we’ll use a Spring Boot’s CommandLineRunner implementation.

Before diving into the implementation details, we’ll introduce some topics. This will help those not familiar with reactive programming. If you already have experience with these concepts, please jump directly into the code explanation.

All the source code (Spring Boot, Angular, Docker) is available on GitHub: Full-Reactive Stack repository. If you find it useful, please star it!

Project Reactor – Main Features

Overview

Project Reactor is a framework built by Pivotal and powered by Spring. It implements Reactive Programming patterns, specifically the Reactive Streams specification.

If you’re familiar with Java 8 Streams, you’ll quickly find many similarities between a Stream and a Flux (or its single-element version, Mono). The main characteristics that make Fluxes and Monos different from the Stream API are that the first two follow a Publisher-Subscriber pattern and implement backpressure.

For instance, if you declare a Flux that takes elements from a database, maps them by applying an operation, and filters them according to some random criteria, then nothing happens. It will do all these operations only when a subscriber subscribes to the Flux. Furthermore, the items flow through the processing logic only when the subscriber is ready to consume more elements. This is the backpressure concept we mentioned earlier in this guide.

As a direct conclusion of this very brief summary, the main advantage of using Reactor is that you’re in total control of the data flow: you can rely on the subscriber’s ability to ask for more information when it’s ready to process it, or buffer some results on the publisher’s side, or even use a full push-approach without backpressure.

The Project Reactor official documents are a good place to start exploring Reactor’s knowledge. In the following sections, we’ll cover some of its features.

Fluxes and Monos

Our application will use the Flux class, an asynchronous sequence of 0-N items. It implements the Publisher interface and is, as we briefly introduced, just a reactive stream that pushes elements whenever the subscriber instructs it to do so.

There is a handy version of a Flux for the special case in which the reactive stream will either emit only one item or none: Mono. It contains a different set of methods to concatenate Mono streams into a Flux.

Reactor Integrations

Spring includes Reactor in some of their popular Spring modules, thus enforcing reactive programming patterns when we use them. Following an intelligent approach, they’re not eliminating the previous programming style in most of these modules. That means we are not forced to adopt reactive programming.

One of the most popular modules where Spring is leveraging Reactor is, as you may guess, the Web framework. Starting with the Spring 5 you can use WebFlux, which comes with major updates like a new way of declaring the controller routes, and transparent support for Server-Sent Events using the Reactor API.

Spring Data has also embraced Reactive Patterns through its Reactive module, with the inclusion of the ReactiveCrudRepository. That means we can perform reactive queries to databases like MongoDB, which already has a reactive version of its driver. Then, the database driver pushes data as a flow controlled by the subscribers instead of simultaneously pushing the query results.

WebFlux – Main Features

Standard Controllers and Router Functions

Spring WebFlux comes with an extra new feature called Router Functions. The idea is to apply Functional Programming to the Web layer and eliminate the declarative way of implementing Controllers and RequestMapping’s – see the full docs here. However, as you can see depicted in the picture below (based on the one available on the official docs), Spring 5 still allows you to use the @Controller and @RequestMapping annotations, so you can decide.

Spring WebFlux Alternatives
Spring WebFlux Alternatives

In this guide, we’ll use the classic, simple way of declaring routes and controllers: declarative annotations. The main reason is that I don’t want to distract you with multiple changes at once, but to be honest, I’m not yet convinced about the convenience of that new style of declaring routes (feel free to judge for yourself).

The dotted orange frame in the figure above represents the stack we’ll use: declarative annotations with WebFlux and reactive streams.

WebClient

WebFlux comes with a reactive web client too: WebClient. You can use it to perform requests to controllers that use reactive streams and benefit from backpressure as well. It’s a kind of reactive and fluent version of the well-known RestTemplate.

As described in the first part of this guide, we won’t use WebClient. That would only be useful for backend-to-backend communication, and it’s straightforward to use. Instead, we’ll create an actual frontend with Angular to consume the reactive API. We’ll detail the differences between a frontend client and a WebFlux’s WebClient.

Creating the application

We won’t demonstrate how to initialize the Spring Boot application to keep this post short. You can clone the GitHub repo or get the full version of the book.

Designing our app

You’ll create a typical three-layered backend application. The exception here is that there is no business logic involved, so the Controllers will use the repositories directly. Besides, we’ll create two co-existing stacks: the reactive and the classic. In any case, we’ll focus on the reactive side while explaining the ideas. See the image for the final structure of the backend application (as you can find it in the GitHub repo).

Reactive Project Structure
Reactive Project Structure

We have duplicated the Repository and the Controller elements as our main logic. The Quote is our domain class. We intend to expose a list of Quotes using a Reactive Web API – with QuoteReactiveController and QuoteMongoReactiveRepository – and compare it with the classic blocking REST API – QuoteBlockingController and QuoteMongoBlockingRepository. The CorsFilter configuration class enables cross-origin requests to simplify the connection from the frontend, and we create the data loader (QuijoteDataLoader) to put the quotes in the Mongo database if they’re not there yet.

We’ll implement two endpoints per controller: one to retrieve all the quotes available in the repository and another that supports pagination. This will allow us to compare both approaches in a more realistic scenario, in which pagination is usually required.

Spring WebFlux Reactive UML
Spring WebFlux Reactive UML

Repository Layer

Let’s start from the source of our Reactive Data Stream: MongoDB. We need to create a class that represents the domain object, Quote, and a Spring Data Repository to collect and map this data to Java objects.

The Quote class

Below, you can find the Quote class implementation. It contains an identifier, the book title, and the quote contents.

package com.thepracticaldeveloper.reactiveweb.domain;

public final class Quote {

    private String id;
    private String book;
    private String content;

    // Empty constructor is required by the data layer and JSON de/ser
    public Quote() {
    }

    public Quote(String id, String book, String content) {
        this.id = id;
        this.book = book;
        this.content = content;
    }

    public String getId() {
        return id;
    }

    public String getBook() {
        return book;
    }

    public String getContent() {
        return content;
    }

    @Override
    public String toString() {
        return "Quote{" +
                "id='" + id + '\'' +
                ", book='" + book + '\'' +
                ", content='" + content + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Quote quote = (Quote) o;

        if (id != null ? !id.equals(quote.id) : quote.id != null) return false;
        if (book != null ? !book.equals(quote.book) : quote.book != null) return false;
        return content != null ? content.equals(quote.content) : quote.content == null;
    }

    @Override
    public int hashCode() {
        int result = id != null ? id.hashCode() : 0;
        result = 31 * result + (book != null ? book.hashCode() : 0);
        result = 31 * result + (content != null ? content.hashCode() : 0);
        return result;
    }
}

Reactive Repository with Spring 5 and Spring Data

Creating a bare Reactive repository is as simple as creating a classic one in Spring Data: you just need to create an interface that extends ReactiveCrudRepository, which is the reactive version of CrudRepository. You’ll have access then to default methods to create, read, update, and delete (CRUD) Quotes.

Additional resources

If you’re curious about Spring Data and how it can be used in a typical Spring Boot application for easy management of databases, consider getting a copy of my new book: Learn microservices with Spring Boot 2nd Edition.

Let’s look at the interface, and then we’ll describe what it does.

package com.thepracticaldeveloper.reactiveweb.repository;

import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
import reactor.core.publisher.Flux;

public interface QuoteMongoReactiveRepository extends ReactiveSortingRepository<Quote, String> {

    Flux<Quote> findAllByIdNotNullOrderByIdAsc(final Pageable page);
}

The interface we’re extending, ReactiveSortingRepository, adds sorting capabilities to the base ReactiveCrudRepository, which contains the basic CRUD operations. We get everything we need except for one requirement: retrieving Quotes pages. To accomplish that, we use Spring Data’s Query Methods, and we pass a Pageable argument to define the offset and the results per page. Note that the query will match all the quotes since our filter looks for non-null ids so it’s there only because the query method findAllBy… always expects a filter. We also want to sort the results by quote ID, so we add the OrderByIdAsc suffix, and Spring Data will translate this to a proper MongoDB sort clause.

Both the provided findAll method and the findAllByIdNotNullOrderByIdAsc that we’ll use from the controller return a Flux. That means that our subscriber (the controller) can control how fast the data should be pulled from the database.

Saving entities in a reactive way

One thing that draws the attention of people not familiar with reactive patterns is how the save method (and its variants) works. Let’s compare the CrudRepository and the ReactiveCrudRepository signatures:

public interface CrudRepository<T, ID> extends Repository<T, ID> {

    <S extends T> S save(S entity);
    
    // ... other methods
}
public interface ReactiveCrudRepository<T, ID> extends Repository<T, ID> {

    <S extends T> Mono<S> save(S entity);
    
    // ... other methods
}

Now imagine that we want to save a Quote, and we’re so confident about the result that we just ignore it, so we use:

quoteRepository.save(quote);

You can do that with an interface extending CrudRepository and the entity will be persisted. However, if you do that with a ReactiveCrudRepository, the entity is not saved. The reactive repository returns a Mono, which is a Publisher, so it won’t start working until you subscribe to it. If you want to mimic the blocking behavior that CrudRepository offers, you need to call instead:

quoteRepository.save(quote).block();

But then you are not leveraging the reactive advantages, and you could keep it more straightforward with a classic repository definition. We’ll see an example of this when we get to the QuijoteDataLoader class.

The Reactive Controller

Controller’s Code

Let’s focus on the most important part of our backend application for the purpose of this guide: the Reactive Controller. First, let’s view the full code source, and then we’ll navigate through the different parts.

package com.thepracticaldeveloper.reactiveweb.controller;

import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import com.thepracticaldeveloper.reactiveweb.repository.QuoteMongoReactiveRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.time.Duration;

@RestController
public class QuoteReactiveController {

    private static final int DELAY_PER_ITEM_MS = 100;

    private final QuoteMongoReactiveRepository quoteMongoReactiveRepository;

    public QuoteReactiveController(final QuoteMongoReactiveRepository quoteMongoReactiveRepository) {
        this.quoteMongoReactiveRepository = quoteMongoReactiveRepository;
    }

    @GetMapping("/quotes-reactive")
    public Flux<Quote> getQuoteFlux() {
        return quoteMongoReactiveRepository.findAll().delayElements(Duration.ofMillis(DELAY_PER_ITEM_MS));
    }

    @GetMapping("/quotes-reactive-paged")
    public Flux<Quote> getQuoteFlux(final @RequestParam(name = "page") int page,
                                     final @RequestParam(name = "size") int size) {
        return quoteMongoReactiveRepository.findAllByIdNotNullOrderByIdAsc(PageRequest.of(page, size))
                .delayElements(Duration.ofMillis(DELAY_PER_ITEM_MS));
    }

}

If you’re familiar with Spring Controllers and their annotations, you’ll find out quickly that the only part of the code that seems different is the Flux object we’re returning as a result of the methods. In Spring MVC, we would probably return a Java collection (e.g., List) instead. Let’s park the delayElements method and the paging arguments for a moment; we’ll cover them in the following sub-sections.

You can also choose the Router Functions alternative instead of the annotated controllers (@RestController and @GetMapping annotations). The implementation would be pretty different, but the resulting functionality would be the same. Since that doesn’t add any value to our code, I’m sticking to the classic style.

The controller calls the QuoteMongoRepository to retrieve all quotes. The printed versions of Don Quixote can quickly get to more than 500 pages, so you can imagine that there are a lot of quotes (more than 5000). Thanks to the reactive approach, we don’t need the complete list of results to be available in the backend before getting them: we can consume the quotes one by one as soon as the MongoDB driver publishes results.

Simulating poor performance

To evaluate the Reactive Web properly, we should simulate issues like an irregular network latency or an overloaded server. To keep it simple, we’ll go for the latter and mimic a situation where every quote has a processing time of 100 milliseconds. It will take almost ten minutes (more than 5000 quotes divided by 100ms each) to retrieve the whole dataset. If you want to try a smaller set of quotes, you can limit the Flux using the method take at the end of the expression.

A simulated delay will also help us visualize the differences between Reactive and MVC strategies. We’ll run the client and server on the same machine, so if we don’t introduce the delay, the response times will be so good that it would be hard to spot the differences.

Pagination

When we created the Repository, we introduced another method to retrieve paginated results. We’re exposing that behavior in the Controller, mapped to the URL /reactive-quotes-paged?page=x&size=y.

Creating pages for results is always a good practice, whether you’re calling the backend in a blocking or non-blocking style. On the one hand, the client might not be interested in getting all the results at once; on the other hand, you want to make the best use of resources by keeping the request’s processing time as short as possible.

We build a Pageable object from the query parameters just by calling PageRequest.of(page, size).

Enabling CORS in Spring WebFlux

We want to connect to the backend from a client application deployed at a different origin because it uses a different port (at least during development). In Web terms, that means the front end will make a Cross-Origin Request. Unless we explicitly allow CORS (Cross-Origin Resource Sharing) in our configuration, that connection will be rejected.

To enable CORS with WebFlux globally, one of the options is to inject a CorsWebFilter bean in the context with a custom configuration of the allowed origins, methods, headers, etc. We’ll configure it to allow any method and header from the origin localhost:4200, where our frontend will be deployed.

package com.thepracticaldeveloper.reactiveweb.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;

@Configuration
public class WebConfiguration {

    @Bean
    CorsWebFilter corsFilter() {

        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://localhost:4200");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }

}

Returning a Flux from a Controller: behind the scenes

Let’s dive a bit more into details. The Spring WebFlux documentation lists the reactive types (Flux included) as possible return types. So, what happens when we request content from the server? How does WebFlux convert the data into a valid response? It depends on how we request it:

  • If we request content without using an Accept header, or we set it to application/json, we’ll get a blocking process and a JSON-formatted response.

  • If we want to go Full Reactive and use the Server-Sent Events support in Spring to implement our full reactive stack, we have to support an Event-Stream response. To do that, we have to set (explicitly or implicitly) the Accept header to text/event-stream, therefore activating the reactive functionality in Spring to open an SSE channel and publish server-to-client events.

We’ll test these two different cases at the end of this chapter.

The Blocking Controller and Repository

To better show the comparison between the blocking and reactive approaches, let’s create a separate Controller with different request paths and connect them to a standard CrudRepository. This code is straightforward and well-known to all developers familiar with Spring Web MVC.

Let’s see the Controller’s code. Note that we also apply the same delay as in the reactive approach. This time, the delay is just a big one since the query is also returning results at once.

package com.thepracticaldeveloper.reactiveweb.controller;

import com.thepracticaldeveloper.reactiveweb.domain.Quote;
import com.thepracticaldeveloper.reactiveweb.repository.QuoteMongoBlockingRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class QuoteBlockingController {

    private static final int DELAY_PER_ITEM_MS = 100;

    private final QuoteMongoBlockingRepository quoteMongoBlockingRepository;

    public QuoteBlockingController(final QuoteMongoBlockingRepository quoteMongoBlockingRepository) {
        this.quoteMongoBlockingRepository = quoteMongoBlockingRepository;
    }

    @GetMapping("/quotes-blocking")
    public Iterable<Quote> getQuotesBlocking() throws Exception {
        Thread.sleep(DELAY_PER_ITEM_MS * quoteMongoBlockingRepository.count());
        return quoteMongoBlockingRepository.findAll();
    }

    @GetMapping("/quotes-blocking-paged")
    public Iterable<Quote> getQuotesBlocking(final @RequestParam(name = "page") int page,
                                              final @RequestParam(name = "size") int size) throws Exception {
        Thread.sleep(DELAY_PER_ITEM_MS * size);
        return quoteMongoBlockingRepository.retrieveAllQuotesPaged(PageRequest.of(page, size));
    }
}

The Repository is very similar to the reactive version, but this time we extend PagingAndSortingRepository, which adds Paging and Sorting functionality on top of the basic CrudRepository. As you can see, we return a List in this case:

package com.thepracticaldeveloper.reactiveweb.repository;

import java.util.List;

import com.thepracticaldeveloper.reactiveweb.domain.Quote;

import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface QuoteMongoBlockingRepository extends PagingAndSortingRepository<Quote, String> {

    List<Quote> findAllByIdNotNullOrderByIdAsc(final Pageable page);
}

Loading data into MongoDB with an ApplicationRunner

We have all the code we need to run our Spring Boot application. However, the Quotes aren’t stored in the database yet. We’ll solve this by reading them from a text version of the book and storing them in MongoDB the first time the application runs.

In the project’s GitHub repository, you’ll see a file containing the ebook in text mode: pg2000.txt. The first time we run the application, every paragraph will be stored as a Quote in MongoDB. To achieve this, we inject an ApplicationRunner implementation in the application’s context: the QuijoteDataLoader class.

You can learn more about how the ApplicationRunner approach works in the Spring Boot’s reference documentation.

In our case, we’ll first check to see if the data is already there. If it isn’t, we create a Flux from a BufferedReader stream, and, for each line, we convert it to a Quote object and store it in the database. For the identifiers, we use a functional Supplier interface to generate a sequence.

package com.thepracticaldeveloper.reactiveweb.configuration;
// ... imports

@Component
public class QuijoteDataLoader implements ApplicationRunner {

    private static final Logger log = LoggerFactory.getLogger(QuijoteDataLoader.class);

    private final QuoteMongoReactiveRepository quoteMongoReactiveRepository;

    QuijoteDataLoader(final QuoteMongoReactiveRepository quoteMongoReactiveRepository) {
        this.quoteMongoReactiveRepository = quoteMongoReactiveRepository;
    }

    @Override
    public void run(final ApplicationArguments args) {
        if (quoteMongoReactiveRepository.count().block() == 0L) {
            var idSupplier = getIdSequenceSupplier();
            var bufferedReader = new BufferedReader(
                    new InputStreamReader(getClass()
                            .getClassLoader()
                            .getResourceAsStream("pg2000.txt"))
            );
            Flux.fromStream(
                    bufferedReader.lines()
                            .filter(l -> !l.trim().isEmpty())
                            .map(l -> quoteMongoReactiveRepository.save(
                                    new Quote(idSupplier.get(),
                                            "El Quijote", l))
                            )
            ).subscribe(m -> log.info("New quote loaded: {}", m.block()));
            log.info("Repository contains now {} entries.",
                    quoteMongoReactiveRepository.count().block());
        }
    }

    private Supplier<String> getIdSequenceSupplier() {
        return new Supplier<>() {
            Long l = 0L;

            @Override
            public String get() {
                // adds padding zeroes
                return String.format("%05d", l++);
            }
        };
    }
}

The data loader is a good example of using a reactive programming style with blocking logic and self-subscription since the ApplicationRunner interface is not prepared for a reactive approach:

  • Since the repository is reactive, we need to block() to wait for the result of the one-element publisher (Mono) containing the number of quotes in the repository (the count method).

  • We apply a reactive pattern to subscribe to the result of save() from the reactive repository. Remember that if we don’t consume the result, the quote is not stored.

If the ApplicationRunner interface would offer a reactive signature, meaning a Flux or Mono return type, we could subscribe to the count() method instead and chain the mono and fluxes. Instead, we need to call block() in our example to keep the runner executor thread alive. Otherwise, we wouldn’t be able to load the data before the executor finishes.

In the loader case, switching to a purely functional approach with the classic repository would make more sense. However, we used this as an example of how fluxes work in a blocking programming style.

Running the backend

Remember that the complete source code (Spring Boot, Angular, Docker) is available on GitHub: Full-Reactive Stack repository. If you find it useful, please star it!

Running MongoDB with Docker

This step is optional; you can also install MongoDB on your machine. It’ll work the same way. However, in the guide’s appendix, you’ll see how to run the complete reactive system using Docker, so it’s good to follow this approach to get first contact with this tool.

You need to install Docker if you haven’t done it yet. Then, create a file named docker-compose-mongo-only.yml with this content:

version: "2"

services:
  mongo:
    image: mongo:3.4
    hostname: mongo
    ports:
      - "27017:27017"
    volumes:
      - mongodata:/data/db

volumes:
  mongodata:

I’ll explain the contents in more detail in the dedicated Docker section in this guide. For now, just remember that this will create a docker container with a MongoDB instance in your Docker host (which can be localhost or a virtual machine IP) and expose the default connection port, 27017. It’ll create a persistent volume, too, so the quotes’ data will remain after stopping the container. To run mongo with this configuration, you need to execute:

$ docker-compose -f docker-compose-mongo-only.yml up

Running the Spring Boot Reactive application

We now run our backend application to see if everything works as expected. To do so, we can either use our preferred IDE to run the ReactiveWebApplication class, or we can use the included maven wrapper from the spring-boot-reactive-web folder:

$ ./mvnw spring-boot:run

If everything goes well, you should see the application starting and, immediately after, the loader storing all quotes. This will happen only the first time you run the backend application.

INFO 36399 --- [           main] c.t.reactiveweb.ReactiveWebApplication   : Starting ReactiveWebApplication on /target/classes started by moises in /frs/
INFO 36399 --- [           main] c.t.reactiveweb.ReactiveWebApplication   : No active profile set, falling back to default profiles: 
INFO 36399 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Reactive MongoDB repositories in
INFO 36399 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 57ms. Found 1 ry interfaces.
INFO 36399 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data MongoDB repositories in DEFAULT mode.
INFO 36399 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 5ms. Found 1 aces.
INFO 36399 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
INFO 36399 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
INFO 36399 --- [           main] c.t.reactiveweb.ReactiveWebApplication   : Started ReactiveWebApplication in 1.911 seconds (JVM running 
INFO 36305 --- [           main] c.t.r.configuration.QuijoteDataLoader    : New quote loaded: Quote{id='03142', book='El Quijote', el mancebo-, yo llevo en este envoltorio unos greguescos de terciopelo, compañeros desta ropilla; si los gasto en el camino, no me podré udad, y no tengo con qué comprar otros; y, así por esto como por orearme, voy desta manera, hasta alcanzar unas compañías de infantería de aquí, donde asentaré mi plaza, y no faltarán bagajes en que caminar de allí adelante hasta el embarcadero, que dicen ha de ser en ener por amo y por señor al rey, y servirle en la guerra, que no a un pelón en la corte. '}
INFO 36305 --- [           main] c.t.r.configuration.QuijoteDataLoader    : New quote loaded: Quote{id='03143', book='El Quijote', merced alguna ventaja por ventura? -preguntó el primo. '}
INFO 36305 --- [           main] c.t.r.configuration.QuijoteDataLoader    : New quote loaded: Quote{id='03144', book='El Quijote', ervido a algún grande de España, o algún principal personaje -respondió el mozo-, a buen seguro que yo la llevara, que eso tiene el servir elo suelen salir a ser alférez o capitanes, o con algún buen entretenimiento; pero yo, desventurado, serví siempre a catarriberas y a ón y quitación tan mísera y atenuada, que en pagar el almidonar un cuello se consumía la mitad della; y sería tenido a milagro que un paje na siquiera razonable ventura. '}
...

Playing with Reactive and Classic endpoints

Let’s test our API endpoints using cURL, a command-line tool that supports Server-Sent Events, so that we can look at the blocking and non-blocking alternatives in our application. On Windows, you’ll need either the native Linux shell or a Linux shell simulator (like Git Bash, included with the official Git distribution package, or Cygwin). You can also download cURL for Windows from this site or use any other HTTP client of your choice (e.g., Postman).

Then we try the paged version of the reactive endpoint:

$ curl "http://localhost:8080/quotes-reactive-paged?page=0&size=50"

Something is going wrong with that request. It takes at least 5 seconds to complete (due to the delay of 100ms per element). After that time, we’ll get an enormous JSON output in the console. It’s still a blocking call.

The reason is that we didn’t specify the contents we accept in our first request so Spring WebFlux, by default, is returning us a JSON and treating the request as a blocking one. To activate the Reactive patterns via a Server-Sent Events channel, we need to request a text/event-stream content instead. Let’s do this:

$ curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-reactive-paged?page=0&size=50"
data:{"id":"00000","book":"El Quijote","content":"El ingenioso hidalgo don Quijote de la Mancha"}

data:{"id":"00001","book":"El Quijote","content":"TASA "}

data:{"id":"00002","book":"El Quijote","content":"Yo, Juan Gallo de Andrada, escribano de Cámara del Rey nuestro señor, de los que residen en su Consejo, certifico y doy fe que, habiendo visto por los señores dél un libro intitulado El ingenioso hidalgo de la Mancha, compuesto por Miguel de Cervantes Saavedra, tasaron cada pliego del dicho libro a tres maravedís y medio; el cual tiene ochenta y tres pliegos, que al dicho precio monta el dicho libro docientos y noventa maravedís y medio, en que se ha de vender en papel; y dieron licencia para que a este precio se pueda vender, y mandaron que esta tasa se ponga al principio del dicho libro, y no se pueda vender sin ella. Y, para que dello conste, di la presente en Valladolid, a veinte días del mes de deciembre de mil y seiscientos y cuatro años. "}

data:{"id":"00003","book":"El Quijote","content":"Juan Gallo de Andrada. "}

...

Much better! The output format is different now, and so is the time it takes to get each quote on the console. This time, WebFlux is creating a Server-Sent Events connection and publishing a quote every 100 milliseconds. The cURL tool supports SSE, so it’s consuming these events. We’re not employing any kind of backpressure here, so our client is just pulling data continuously and printing it on screen.

Let’s keep that header in our request and evaluate what happens in the rest of the cases:

curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-blocking-paged?page=0&size=50"

This call will block for five seconds since we’re requesting data to the classic controller (non-reactive). The same will happen if we change the URL to http://localhost:8080/quotes-blocking. In that case, you’ll need some patience since it will take almost ten minutes to complete.

curl -H "Accept: text/event-stream" "http://localhost:8080/quotes-reactive"

That one will also take almost ten minutes to finish but, in this reactive scenario, we’re getting quotes as soon as they’re dispatched. If you’re a fast reader, you can try to read the book as it’s printed to console. Otherwise, you can always cancel it with a Ctrl-C.

At this point, we proved one of the most essential takeaways from this guide: it doesn’t matter if you use a Reactive Web approach in the backend; it won’t be reactive and non-blocking unless your client can handle it as well.

We’ll implement a more realistic Reactive Web Client in the next section of this guide: the Frontend.

Continue Reading This Series

This article is part of the Full Reactive Stack series:

  1. Part 1: Introduction
  2. Part 2: The Backend - Spring Boot, WebFlux and MongoDB (this post)
  3. Part 3: The Angular Frontend
  4. Part 4: Conclusions

Want the complete guide? Get This Blog Series and More as a Comprehensive eBook—Available Now for Purchase!

Master Microservices with Spring Boot

A hands-on guide to building production-ready microservices

Get the Book

Get More Architecture Insights

Join hundreds of software architects and engineers getting practical insights delivered to their inbox.

Back to Blog