hbrown.dev

Welcome to my developer page. Contact me on: henry.g.brown@hey.com

View on GitHub
10 April 2021

Why you may want to consider building your next API reactively

by Henry Brown

In my last post, I looked at RSocket and one of the items I glossed over was the use of the reactive type: Mono. As I mentioned in the accompanying video, reactive programming can take some getting used to if you have not done it before.

In my post today, I would like to start looking at reactive programming but instead of jumping into the details, I would like to start by showing you why you may want to invest your time into learning a new programming style.

To do that, I will compare 2 ways of building a simple REST API using Spring Boot. First, I will build a single endpoint using Spring MVC and then compare it to building the same functionality using Spring Webflux. I will not spend too much time on the implementation details but would like to highlight the following elements:

  1. How simple it is to convert a simple Spring MVC API to a Spring WebFlux API; and
  2. What advantages are to be gained by the transition.

As usual, the easiest way to follow along is to get the code from my Github repository.

In the linked repository, you will find that each approach is in its own branch named: springmvc and webflux respectively.

The Spring MVC application

The application has a single entity:

class Contact(
    val id: Long? = null,
    val name: String,
    val mobileNumber: String,
)

and defines a single endpoint with the path: /contact to retrieve all the Contact entities. It uses the common approach of defining a RestController to create the endpoint and a Spring Data JPA Repository to implement the persistence requirements.

The controller defining the endpoint looks as follows:

@RestController
@RequestMapping("/contact")
class ContactController(
    private val contactService: ContactService,
) {
    @GetMapping(produces = [ MediaType.APPLICATION_JSON_VALUE])
    fun list(): List<Contact> = contactService.findAll()
}

with a dependency on a ContactService that exposes the single method to list all the contacts:

interface ContactService {
    fun findAll(): List<Contact>
}

@Service
class ContactServiceImpl(
    private val contactRepository: ContactRepository,
) : ContactService {

    private val log: Logger = LoggerFactory.getLogger(javaClass)

    override fun findAll(): List<Contact> = contactRepository
        .findAll()
        .map {
            log.info("Processing element ${it.id}")
            Thread.sleep(500) //pretend we have some important work to do
            it
        }
}

To simulate actual work being done in the service tier, we will simply let the thread sleep for half a second for each entity returned. In a real-life application, this is usually where the business rules will be applied before sending the results back to the client.

The only part left is to define the interface that will be implemented by Spring Data JPA to fetch the data from the database:

interface ContactRepository : JpaRepository<Contact, Long>

And that is the entire application.

The Database

To simplify the creation of the database and some test data, a Dockerfile is provided that will stand-up a PostgreSQL server and populate it with data. The image for the data can be pulled from my public Docker Hub repository and run using the following command:

docker run --name db-contact -e PGDATA=/tmp -d -p 15432:5432 -v ${PWD}:/var/lib/postgresql/data henrygbrown/db-contact

Once, the database is running, you can access the database running inside the container using:

docker exec -it db-contact psql -U postgres -d contact_db

Executing the following command against the database should show the 50 records that have been loaded:

select count(*) from contact;

Now that we have a fully-functional Spring MVC application, let’s turn our attention to creating a WebFlux application.

Converting the Spring MVC application to use WebFlux

The first step in converting the application is choosing the correct dependencies. Instead of selecting org.springframework.boot:spring-boot-starter-web (for Spring MVC) we choose webflux: org.springframework.boot:spring-boot-starter-webflux.

We also change the dependency from Spring Data JPA (org.springframework.boot:spring-boot-starter-data-jpa) to org.springframework.boot:spring-boot-starter-data-r2dbc. This also requires us to change the JDBC driver from org.postgresql:postgresql to io.r2dbc:r2dbc-postgresql.

The final change required for the database, is to change the configuration in application.properties from:

spring.datasource.url=jdbc:postgresql://${POSTGRES_SERVER:localhost}:${POSTGRES_PORT:15432}/${POSTGRES_DB:contact_db}
spring.datasource.username=${POSTGRES_USERNAME:postgres}
spring.datasource.password=${POSTGRES_PASSWORD:postgres}

to:

spring.r2dbc.url=r2dbc:postgresql://${POSTGRES_SERVER:localhost}:${POSTGRES_PORT:15432}/${POSTGRES_DB:contact_db}
spring.r2dbc.password=${POSTGRES_PASSWORD:postgres}
spring.r2dbc.username=${POSTGRES_USERNAME:postgres}

In the Contact entity, we can also lose all the JPA configuration:

import org.springframework.data.annotation.Id
class Contact(
    @Id
    val id: Long? = null,
    val name: String,
    val mobileNumber: String,
)

The major change in the Repository is changing the JpaRepository to extend the ReactiveCrudRepository:

import org.springframework.data.repository.reactive.ReactiveCrudRepository
interface ContactRepository : ReactiveCrudRepository<Contact, Long>

The implementation of the ContactService uses the Flux type from Project Reactor. Like Mono used in the previous article, Flux is a Publisher that can be used to stream values to subscribers. The implementation of our service becomes:

@Service
class ContactServiceImpl(
    private val contactRepository: ContactRepository,
) : ContactService {

    private val log: Logger = LoggerFactory.getLogger(javaClass)

    override fun findAll(): Flux<Contact> = contactRepository
        .findAll()
        .delayElements(ofMillis(500)) //pretend we have some important work to do
        .doOnNext { log.info("Processing element ${it.id}") }
}

Just like the Spring MVC implementation we delay each element by half a second to simulate work being done on the returned entities.

The final change is in the ContactController class in which we have to change the return type from a List to a Flux as well. The new implementation is as follows:


@RestController
@RequestMapping("/contact")
class ContactController(
    private val contactService: ContactService,
) {

    @GetMapping(
        produces = [ MediaType.TEXT_EVENT_STREAM_VALUE]
    )
    fun stream(): Flux<Contact> = contactService.findAll()
}

This concludes all the changes required to transform our Spring MVC API into a Reactive API using WebFlux.

What do we get for our effort?

Hopefully, you will agree with me that changing an endpoint from using Spring MVC to WebFlux is not a challenging task. Spring does a great job of allowing us to use the same skills acquired building APIs using Spring MVC to build APIs that are reactive. But this alone is not enough reason to want to switch and learn a new programming paradigm. The real benefit of this approach, can be seen when we run the application.

If we start the Spring MVC application and use curl to fire a request against our endpoint:

curl -X GET --location "http://localhost:8080/contact" -H "Accept: application/json"

You will notice that the request takes a long time before any response is returned (about 25 seconds). Looking at the logs, you can see the processing of each element about half-a-second apart. The time taken for the response is thus hardly a surprise.

On the other hand, if we run the same request against our reactive endpoint we notice something different:

curl -X GET --location "http://localhost:8080/contact" -H "Accept: text/event-stream"

This time, we see the elements being logged as soon as they are processed. There is no long delay from when the request is issued and when we start receiving data.

As I am sure you can imagine, this difference in performance would make quite a dramatic impact on the client-experience of a front-end.

Another major advantage of the 2 different approaches can be seen if the request is cancelled before it is completed. If you cancel the request to the Spring MVC application and look at the logs, you will notice that the cancellation has no effect on the server itself. The server still continues and completes all the work from the request even though it is no longer needed by the client.

Performing the same action on the WebFlux endpoint though shows that when the request is cancelled, the server stops processing the request as well without fetching and processing additional records from the database.

I think you will agree that for very little effort on our part, we have switched to an implementation that is not only much more efficient on our server but will also dramatically improve the experience for users of the API.

Conclusion

In this article, we have seen how simple it is to transform an API built using Spring MVC to use WebFlux. We have seen that building an API using these reactive types can lead to a much more efficient use of resources on the server and provide a dramatically better experience for users of the API.

As usual, the code for this project is available on my Github repository. You can also get the docker image for the database used in this project.

Finally, if you would prefer watching videos to read, the same content is accessible on my YouTube channel: https://youtu.be/6N1pnYun2AE

tags: kotlin - springboot - springmvc - springboot - reactive