Reactive or Coroutines: Between a rock and a hard place?

Benedikt Jerat
Jul 23 · 8 min read
Red pill or blue pill? Photo: Courtesy of Warner Bros.

Today, at peak times customer facing applications often have to cope with tens of thousands or more simultaneous requests. While cloud computing and the accompanying possibilities of dynamic scaling of applications help to overcome this, they also reach their limits at some point. Also the long held equivalence of “one thread per request” is outdated in this context.

Commonly, threads require between 128 and 256 KB of memory per thread, at least on the JVM. Regardless of scaling possibilites, with an increasing number of requests, the limit of what is reasonable is quickly reached. Garbage collector cycles would not necessarily be shorter with a larger heap.

In practice, different approaches exist for a more efficient use of resources. In this article I would like to compare two concepts and work out their advantages and disadvantages with the help of a small programming example. These two concepts are Reactive Programming and Coroutines.

Some background

Before we get into the implementation, I would like to briefly explain the two concepts.

Reactive programming attempts to minimise the required concurrent number of threads through intelligent scheduling. Reactive programming works with asynchronous functional chains that propagate inputs from the producers along these functional chains to the receivers. Reactive frameworks are completely abstracting away the entire underlying thread and scheduling model from the developer. This will result in more efficient use of resources by a number of factors. However, anyone who has ever worked with reactive streams will quickly realise that working with them is anything but easy due to this level of abstraction. For reactive streams, the code base is organised in the form of functional call chains.

An alternative approach to the efficient use of resources has been established in the form of coroutines. This concept from asynchronous programming dates back to the 60s and is now finding its way into more and more programming languages. For example, Kotlin has supported coroutines natively since version 1.1. The concept also allows asynchronous code execution. However, the asynchronous code still looks like it is being executed sequentially.

Coroutines do not require you to restructure the code base and allow an imperative and sequential programming style. However, reactive frameworks offer predefined operators for almost every problem. As both approaches have advantages and disadvantages, let’s compare them with a more extensive example.

Setting things up

First things first, all the code used in the example can be found on our Github repository: https://github.com/dxfrontiers/coroutines-vs-reactive

The code is written in Kotlin. If you don’t know the language, I recommend reading my colleague’s blog post on why Kotlin is simply better (and more fun!) than Java.

For the reactive part, I’m using the reactive triad Project Reactor, Spring WebFlux and R2DBC, so that every part of the application is non-blocking, from the web layer down to the persistence. For the basics of Project Reactor and Spring WebFlux take a short look into on of my former blog posts “Reactive Programming in a Nutshell”. R2DBC (Reactive Relational Database Connectivity) brings reactive programming APIs to relational databases. In our case, we’re using the Spring Boot Data R2DBC integration for the H2 embedded database.

For the coroutines part, the Kotlinx Coroutines library is used that brings coroutines to Kotlin as third-party library. As coroutines can be seamlessly used along with Spring WebFlux and Spring Data R2DBC, no additional frameworks are required.

In detail, the used frameworks and their corresponding versions are:

  • Kotlin 1.4.32
  • Kotlinx Coroutines 1.4.2
  • Spring Boot + WebFlux 2.4.2
  • Project Reactor 3.4.2
  • R2DBC 0.8.4.RELEASE
  • Spring Data R2DBC 1.2.3

Get me to the code!

The whole example consists of one microservice with a REST endpoint, a minimalistic service layer, and a persistence layer for our two entities Character and House. We are building some kind of house affiliation for Game of Thrones characters. Very creative, I know :)

The implementation is done separately for reactive and coroutines, but of course, they share the same endpoint methods to expose.

Path for reactive API: /reactive/characters

Path for coroutines API: /coroutines/characters

Methods to expose for both:

  • Find by name: GET /?lastName=<lastName>
  • Find by id: GET /{id}
  • Add: PUT /?firstName=<firstName>&lastName=<lastName>
  • Delete by name: DELETE /?firstName=<firstName>&lastName=<lastName>

Example invocation:

curl -s "http://localhost:8080/reactive/characters?lastName=Stark"[{"id":1,"firstName":"Eddard","lastName":"Stark","house":1},{"id":6,"firstName":"Arya","lastName":"Stark","house":1}]

The application was only filled with a few sample data.

So, let’s start from the outside and gradually descend further down to the persistence.

Controller (Reactive)

For the most part, the controller is quite straightforward to implement:

The controllers defines four methods for the four methods we want to expose. The findByName method may return multiple characters, so we return a Flux of characters here. The findById hopefully only finds on matching character, so we return a Mono object containing zero or one character.

Implementing the addCharacter method is already a bit more sophisticated, as we have three different outcomes to handle:

  • 201 (CREATED): The character did not exist before and could actually be added
  • 200 (OK): The character already existed and nothing has been changed
  • 400 (BAD_REQUEST): Some input validation error occurred and the request was rejected

Errors cannot be handled by @ExceptionHandler annotated methods, as these would break the reactive workflow. So, we are required to use one of the many OnErrorX methods that the Reactor API provides us.

Lastly, the deleteByName method is straightforward again. Only the return type Mono<Void> looks unfamiliar at first, because we actually don’t want to return anything. However, the framework expects us to return a reactive type, so we encapsulate nothing into a Mono type. Could be worse.

Controller (Coroutines)

For Kotlin coroutines the implementation could look like the following:

The first thing to notice are the different return types, as we’re not using the reactive types anymore. In general, the following mapping can be applied:

  • Mono<T> → T?
  • Flux<T> → Flow<T>
  • Mono<Void> → Void?

Kotlin’s nullable types really come in handy here to simplify the return types. The Flow type encapsulates a stream of values that is asynchronously computed and behaves like a general Kotlin collection with all its amazing operators.

The next thing to notice is obviously the suspend modifier on each method (expect the one returning Flow, because it is internally invoking only suspend functions). This marks these functions as suspendable. That means, the underlying thread isn’t blocked. Suspend functions can actually only be called from a coroutine or another suspend function.

But the most important thing to notice is the actual quite imperative-like programming style that’s possible when implementing with coroutines. For most developers, the implementation will look more similar to what they’re used to see everyday. We can even use a try-catch expression here.

Coroutines allow us to perform asynchronous operations using imperative code.

At least for me, the coroutine implementation was much easier to accomplish than the reactive variant, because it mostly looks like a controller implementation I am writing on a daily basis.

Service Layer (Reactive)

Now it’s getting a bit more spicy for the reactive part of the service layer:

The findByLastName and the findById methods are self-explanatory. Deleting the character in the deleteByName method requires a bit more effort, but looks quite easy for the most part. Here, it is really important to choose the correct operator for the deletion operation. Although, we might think deleting an entry in the database is a side effect operation, because it changes the outer world, and we don’t really care about the feedback from the method, we cannot not use Mono.doOnNext here. The delete method from the ReactiveCrudRepository returns a subscribable value itself, so it is required that anyone™ subscribes on it:

Mono<Void> deleteById(ID id);

Otherwise, nothing would happen, according to the principle: Nothing happens until you subscribe.

We could either subscribe manually to the returned value or pass everything down the layers, until Spring WebFlux handles the subscription for us in the controllers. I opted for the latter and chained the call with the flatMap operator.

For the addCharacter method, the implementation is getting a bit more sophisticated, because three cases need to be handled:

  • The character does not exist and should be added
  • The character already exists and nothing should be changed
  • The character’s house does not exist and an exception should be thrown

The filter in line 19 handles the second case by returning an empty Mono if the predicate applies. For the other cases, we are flat mapping the result from the House resolution and flat mapping again for the chained save method. In case, resolving the House returns no result, we provide an error-Mono that encapsulates our exception. Think of the switchIfEmpty method as the Optional.orElseGet method.

The reactive variant reveals one or two pitfalls to be aware of. Let’s see how the version with coroutines looks.

Service Layer (Coroutines)

With coroutines, the service could be implemented like this:

As before, findByLastName and findById are no-brainers. The deleteByName method is actually quite simple. With the well-known expr1?.let { expr2 } construct, we can perform some operation expr2 only if expr1 returns some value. So, we trigger the deletion only, if some character was found. Coroutines allow us here to stick with common programming patterns (common to Kotlin developers at least).

For the addCharacter method, we can take advantage of Kotlin’s if expression to simplify the structure a bit. If the character was found, we return null and do nothing else. Otherwise, we search for the house first and use the let expression again to save the character, if a house was found. If no house was found, Kotlin’s elvis operator ?: strikes and we just throw some custom exception.

Persistence (Reactive / Coroutines)

Finally, let’s come to the easiest part of the solution, the persistence. Starting with the Spring Data Neumann release train, query derivation finally also works for reactive and coroutine repositories. Yay!

This makes the implementation actually very simple for both.

Reactive repository:

Coroutines repository:

Inherit from the correct interface, write your methods, use the appropriate types, and don’t forget the suspend keyword for the coroutine methods. Pretty self-explanatory.

Trying stuff out

Get all characters:

curl -s -XGET "http://localhost:8080/reactive/characters?lastName=Stark"[{"id":1,"firstName":"Eddard","lastName":"Stark","house":1},{"id":6,"firstName":"Arya","lastName":"Stark","house":1},{"id":10,"firstName":"Sansa","lastName":"Stark","house":1}]

Delete a character:

curl -s -XDELETE "http://localhost:8080/coroutines/characters?firstName=Sansa&lastName=Stark"
curl -s -XGET "http://localhost:8080/coroutines/characters?lastName=Stark"
[{"id":1,"firstName":"Eddard","lastName":"Stark","house":1},{"id":6,"firstName":"Arya","lastName":"Stark","house":1}]

Add a character:

curl -s -XPUT "http://localhost:8080/coroutines/characters?firstName=Ellaria&lastName=Sand"No valid house found for the character Ellaria Sand!

Nobody likes Ellaria anyway ¯\_(ツ)_/¯

Conclusion

Both concepts were able to fulfil the requirements and allowed us to write asynchronous code in a relatively simple manner.

In reactive programming, we are required to stick to the reactive chains and build our workflow around those chains. It really takes some time to get used to the reactive operators, the omnipresent chaining, and the requirement to subscribe to the values (which can be done by the framework, when we use it correctly, but nevertheless a requirement).

The coroutines solution on the other hand always felt a bit more familiar. Although the code is performed asynchronously, it still looks like it is being executed sequentially. We can reuse common programming patterns of the language that make our developers’ life simple and easy.

Even though we have only scratched the surface of these concepts, I would currently tend towards a solution with coroutines for future projects.

Thanks for reading! Feel free to comment or message me, when you have questions or suggestions. You might be interested in the other posts published in the Digital Frontiers blog, announced on our Twitter account.

Digital Frontiers — Das Blog

Dies ist das Blog der Digital Frontiers GmbH & Co.