Java has always been seen as a synchronous language. Traditionally, scaling a java web service meant increasing the thread capacity of the server, either by vertically scaling the machine, or increasing the number of servers.
With Java, there have been ways to implement event based programming using a queue mechanism. But it relied solely on multithreading to achieve these goals. And Java’s multithreading paradigm is very strong. But building asynchronous logic still required integrating multiple systems and handling the complexities which arrive with them.
In this article, we will take a look at how we can achieve asynchronous programming with CompletableFuture introduced in Java 8 and build REST APIs on top of it using Spring Webflux.
A note before we start
This article and the supporting code are nowhere near to be production ready. There are a few significant things like error handling, data validation etc. not covered here.
A note about R2DBC Driver
In this article, we convert the database calls to asynchronous to utilize the time when a thread is waiting. This is achieved using CompletableFuture. But this is just for demonstration purposes. For production application, I propose using R2DBC drive which provides reactive features on top of regular PostgreSQL driver and is much more robust. Check this link for details.
For other databases where a reactive driver is not available, the mechanism mentioned here can be utilized with some modifications.
A little about Spring Webflux and Completable Future
Spring webflux is built with the idea of reactive and non-blocking programming. It is built to leverage the time which goes into waiting for I/O calls to complete. It is important to note that webflux is not built as an alternative to regular spring modules, but to complement them for the situations where this kind of mechanism is needed.
Reactive programming resembles event based programming. There are components waiting for something to happen (like an event) and react to these events, performing certain operations.
CompletableFuture is built as an advancement to Future interface. Both of these are used for asynchronous programming in java. While Future interface has limitations in terms of what can be done with a result of an asynchronous call, CompletableFuture provides an array of APIs for composing, combining and error handling in asynchronous situations. It also provides a way to build a pipeline which will perform all the operations in asynchronous manner, in the order we define.
The POC application
Here, I’ve built an application which has simple CRUD REST APIs. The use case involves Vehicle as the resource. The application is very trivial in nature, but shows the concept of reactive programming with the help of webflux and CompletableFuture. The database layer is the one here which is converted to async nature. The queries are performed inside CompletableFuture, and the results are processed there as well. This object is then used to create Mono or Flux objects, which are the core of Spring Webflux.
Mono and Flux
Mono and Flux are the reactive types, forming the core of the Reactor Framework. There are great articles available online on understanding these two. But in a nutshell, these two are the publishers. These two are used to publish the.results in the asynchronous form. Mono is used whenever we have only one set of data to return, whereas Flux is used when there could be more than one set of data, which would be returned as and when available.
Now this would become complicated with HTTP because there is no direct support for this. And this is where Spring Webflux handles the conversion of these streams to HTTP protocol compliant response.
Alright then. Time for some code. Let’s take a look at the APIs.
The code is structured in a standard way, with a controller layer handling the REST calls and the service layer doing the heavy lifting. The database is handled using JpaRepository.
POST API | Mono Example — Create Vehicle
Usually, we define the controller for any API which returns a ResponseEntity. This allows us to return a valid HTTP response with status code, headers and any response body. With webflux, we can return a Mono, which allows us to define our type.
Now the controller has to return a Mono object. This has to be propagated to the service layer as well. There is no point in the controller creating the Mono. This should be the job of the service. This how the service should look
Now to the logic. We want the database call to be asynchronous and create a Mono object which would react to the database call completion. This is accomplished with the help of CompletableFuture. Let’s take a look at the code
The code above relies on CompletableFuture and the future object is used to create the Mono object as well. The spring webflux framework will now handle the thread mechanism, not waiting for the future to complete before returning from the service method.
To verify this, apply a couple of logs and see the order in which they are printed. Something like this
GET API | Flux Example — Get list of Vehicles
Alright, now let’s take a look at how to use flux. Following is the controller for the API which returns the list of Vehicles created till now
The things to notice here is that the API returns a Flux of Vehicle objects and not a List of them. Following is how the service method would look
Now the logic to generate the stream of data. Let’s look at the code first and then we understand what’s happening
The way we are generating Flux is by creating one explicitly and then emitting the objects generated by the future. Once we have the list of vehicle objects available, we use a callback function of CompletableFuture — whenComplete. Inside this callback, we emit the Vehicle objects received from the future.
Notice here that in case of exception, we just mark the FluxSink as complete. There are way to propagate the errors up the stream as well, like emitter.error(exception). But we will look at error handling in later articles.
Pros and Cons of Asynchronous approach
Let’s look at some of the advantages of asynchronous approach
- CPU cycles — usually when a thread is waiting for a db operation to finish, it’s doing nothing. This means that we have one less thread for handling requests. With async programming, we can delegate this task to a callback. Our thread will be freed and called back when the result is available.
- Load — we can handle more load with the same number of server threads.
- Response time — the response time would be more consistent compared to sync requests under heavy load. Since the requests are handled in async manner, they will not wait because the server threads are busy.
- Time to clear the load — when the system is under heavy load, sync requests would be cleared one by one. Whereas, with async, the requests would be served in parallel, utilising the CPU cycles gone into wait time. Therefore, the overall time that it takes for the server to clear the load would be less with async programming.
But like with every technology, there are some cons as well
- Debugging — when things are happening in asynchronous manner, keeping track of the sequence of events becomes difficult.
- Development — the development itself is not easy with async programming. Stitching the sequence of events with callbacks and handling the errors can become quite complex, especially the readability part.
- Response times — in case the system is under normal load, the response times could be higher compared to synchronous handling. This is because a few milliseconds go into spinning up separate threads and then converting Mono/Flux to HTTP compliant response.
Both synchronous and asynchronous approaches have their place. They both offer some advantages over the other. But as with many other situations, there are some tradeoffs in terms of development time, ease of maintenance and utilizing the resources. In a situation where we think there are going to be a considerable amount of I/O tasks or third party API calls where the program might eat up CPU cycles waiting for these to be complete, asynchronous programming will make more sense.
But in case we are building simple CRUD APIs, talking to the database, it would make more sense to go with a synchronous approach. In most such cases, the database is within the same network and response times are low. Introducing the code complexity will only result in insignificant gains.
And if the decision is not this simple, we can always do some POC and test to compare the results.