DEV Community

Cover image for Don't Drink Too Much Reactive Cool Aid
Jan Wedel
Jan Wedel

Posted on

Don't Drink Too Much Reactive Cool Aid

Cross-posting of my blog article.


In the last years, the idea of "reactive server applications" was being promoted heavily, especially by new frameworks. In the Java world, it's been RxJava, vert.x and recently Spring WebFlux.

While generally liking the underlying ideas, I find the way that it's beeing pushed both by the writer as well as users problematic. After a conversation I had on twitter with Josh Long, I'd like to explain why.

There are a couple of myths around it, most dominantly...

Synchronous code is slow / Reactive code is fast

Is that true?

So first, what does synchronous mean? In the early days, code was executed on a computer in a sequential manner. Instruction by instruction until the code ended. There came the need to run more than one program at a time. This created the need for processes. You can now have multiple programs that run concurrently, not necessarily in parallel. The Processor would simply switch back and forth between the program execution and keep memory separated.
Then, there was the need to do things within one program concurrently. Threads were created that allow to run multiple parts of a program concurrently but allowing shared memory.

Synchronous code would simply run in one process and one dedicated thread until its done.

A reactive programming model assumes a central handling of all things that go in and out of the program (I/O). One thread would typically be doing multiple things, looping through possible things that could happen. Is there a new TCP connection? Is my byte written to a stream? Is the file loaded from disk? Once it's done, it will inform some registered piece of code that it done. This would then be a call-back function.

So, is this synchronous code slow to reactive code? In fact synchronous code is faster because the thread is not used to do different things in between. Like when you're currently writing to a TCP stream, nothing interrupts sending out bytes because some other part of the program wants to read from a file. So assuming an ideal pipe (like infinite I/O throughput), synchronous code would be faster. But most of the time, I/O is significantly slower than computation meaning there is plenty of time to do other stuff while waiting on I/O to complete.

Threads Are Expensive

Now, you have the possibility of using your single thread to do other stuff in the meantime. Why is that an issue? Well, to create a thread, it takes some kilobytes of RAM and switching context between multiple threads is expensive as well.

See here for example. At some point, it eats up all of your memory and CPU.

Reactive Scales Better

In classic web servers, for each incoming web request, a new thread was either started or reused from a pool, and then all the handling, DB calls etc. was done in that thread. That's still no problem until you get higher loads. Because of the RAM usage and some OS limits, you cannot have an infinite number of threads. The limit is probably around a couple of thousand threads. So either you're application goes out of memory (no thread limit enforced) or it gets slower processing responses (limited thread pool) because some requests have to wait until a used thread is freed.

The thing is, to actually hit this limit, you must be some of a lucky person. Even a single instance of a Spring Boot application e.g. can serve a considerable high load with extremely slow response times.

When you hit that limit, you can start to just spawn multiple instances of the application (if it's able to) and use a load balancer.

To put it in another way: A reactive app might have slower response times on low load, but it is able to keep this response time constant for a higher number of incoming requests. It is able to use the existing resources of your server more efficiently.

This is illustrated in a couple of posts you can find here and here.

The Programming Model

While it seems to be a good idea to use asynchronous I/O handling, despite the fact that is not "better" in all scenarios, there is one serious flaw: It completely changes the way you need to write your code. You cannot just "switch on" async in most popular programming languages and good to go.

As I mentioned before, there is the notion of call-backs. So conceptually, instead of doing

    someFunction() {
        value = getValue()
        print(value)
    }
Enter fullscreen mode Exit fullscreen mode

you need to do something like this:

    someFunction() {
        getAsyncValue(callback)
    }

    callback(myValue) {
        print(myValue);
    }
Enter fullscreen mode Exit fullscreen mode

But, this is just the very basic case. It get's worse when you have nested calls..

    someFunction() {
        getAsyncValue(valueCallback)
    }

    valueCallback(myValue) {
        getUserFromDbAsync(dbCallBack)
    }

    dbCallback(user) {
        print(user + myValue);
    }
Enter fullscreen mode Exit fullscreen mode

This is a combination of two calls, and oops, how to I pass myValue to the dbCallback function?

Now I can use lambdas and closures to freeze floating values inside the code:

    someFunction() {
        getAsyncValue(myValue -> {
            getUserFromDbAsync(user -> {
                print(user + myValue)
            })
        })
    }
Enter fullscreen mode Exit fullscreen mode

Is that better? And what about error handling? Easy:

    someFunction() {
        getAsyncValue(myValue -> {
            getUserFromDbAsync(user -> {
                print(user + myValue)
            }).onError(() -> {
                print("can't get user")
            }
        }).onError(() -> {
            print("can't get value")
        })
    }
Enter fullscreen mode Exit fullscreen mode

What if I need to handle the DB user error in my getAsyncValue callback? Have a look at my vert.x 2 review article. It's old but it covers some ideas of this article.

With reactive stacks like WebFlux, you can use chained pseudo-functional calls like:

    someFunction() {
        getAsyncValue()
            .map(myValue -> print);
    }
Enter fullscreen mode Exit fullscreen mode

However, it then gets tricky when you want to combine the calls above and add error handle. You have a long chain of .map, flatMap and zip that won't improve things a lot compared to the async version and are bad compared to the synchronous version. Have a look at the examples here. Actually, one method call-chain is spread across 12 lines, including pseudo if-else calls.

In all of the above cases, using function references, lambdas or reactive extensions, the code suffers in:

  • Readability
    • It's obvious to see that the code is longer
    • It's more complex
    • It doesn't have a sequential flow anymore. Or to put it that way: The code is executed in another order as it is written down in the sources.
  • Maintainability
    • It's hard to add functionality
    • Debugging this code is much more complicated. You cannot simply step through it
  • Testability
    • You cannot simply mock 1 or 2 classes and call a method. You need to created objects that behave synchronously but are actually asynchronous.
  • Code Quality
    • Because it is hard to digest the code by reading, it will have more bugs than synchronous code.
  • Development Speed
    • Very simple tasks like e.g. two REST requests with different return types that need build up on each other (e.g. get master data, enrich with some related data and return merge to UI)
    • Simply put: You cannot just write the way you think

I've written professional applications with vert.x, RxJava and most recently Spring WebFlux and the above issues could be found in all of them in different flavors.

What Now?

What you actually want is code that you can write and read as synchronous but that is executed by the underling runtime in an asynchronous way that support conventional error handling:

    someFunction() {
        try {
            myValue = getAsyncValue()
            user = getUserFromDbAsync()
            print(user + myValue)
        } catch (ComputationException e) {
            print("can't get value")
        } catch (DbException e) {
            print("can't get user")            
        }
Enter fullscreen mode Exit fullscreen mode

Now, you'd have the good path within three lines and separated error handling. The code is very easy to understand. What happens is, that the runtime will park the execution of getUserFromDbAsync until the DB returns the data. In the meantime, the thread would be used for other tasks.

Erlang implements that within it's Actor model and BEAM VM. When you read from a file, you just call io:get_line and get the result. The VM will suspend the current actor and it's Erlang process (which is a light weight thread that takes only a few bytes of memory).

For the JVM, there is currently Project Loom that tries to implement continuations in the JVM and an abstraction called Fiber as part of the JDK. To me, it looks promising, but it will take some time and it's not 100% sure that it will ever be part of Java.

Python has asyncio that provides some more language features more advanced than Java but still, it's too exposed, IMHO.

Conclusion

With my article, I'd like to emphasize the point of reactive programming not being the silver bullet (as it is most often the case). Be sure to know what you're doing and precisely why you need it.

Taken from my discussion with Josh:

It think that there are currently only a handful scenarios that would require to build fully reactive server applications. Otherwise, just stick with the "classic" approach.

The problem is, after you were writing applications with your reactive hammer for some time (which has a steep learning curve), you see every task as a nail. This would be OK if code quality doesn't suffer but it objectively does .

And, to inexperienced developers, it might seem cool because it's so hard to understand. As if only the best developers will understand it. But trust me, this is not the case. The best language and API designs are those, that are easy to understand, write and read.

Top comments (19)

Collapse
 
alainvanhout profile image
Alain Van Hout

Apart from this being a great write-up, the very final paragraph caught my eye:

And, for unexperienced developers, it might seem cool because it's so hard to understand. As if only the best developers will understand it. But trust me, this is not the case. The best language and API designs are those, that are easy to understand, write and read.

Possible the most important yet undervalued insight to be (or become) an excellent software developer.

Collapse
 
leob profile image
leob

Good write-up, however if you use a language like Javascript which doesn't have threads, you will obviously have to do 'reactive', whether you like it or not.

Promises and async/await make it much more manageable, but some of the drawbacks are still there (ever tried to decipher an error stacktrace in a node/express program? it ain't easy ...)

Collapse
 
stealthmusic profile image
Jan Wedel

Jup. That’s why I explicitly mentioned backend programming. ;)

Collapse
 
leob profile image
leob

Yup ... but Javascript gets used on the backend too, obviously ... Node/Express ;)

Thread Thread
 
stealthmusic profile image
Jan Wedel

True :) trying to erase that from my brain whenever I can 😜

Thread Thread
 
leob profile image
leob

Haha, you're not a "JS backend" fan I see ... I can sympathize with that, I'm using node/express on a few projects but quite often I ask myself "WHY ... ?"

Thread Thread
 
stealthmusic profile image
Jan Wedel

Yup, I'm not a "JavaScript for anything" fan Β πŸ˜‡
We're using TypeScript for UI Code, Java in the backend.

Thread Thread
 
leob profile image
leob

Wise decision, saving yourself a lot of trouble ... yes and classical threads are fine for the majority of purposes

Collapse
 
hazer profile image
Vithorio Polten

Great write-up, sharing with my team :)

For the JVM now there's Kotlin Coroutines. They also have the concept of suspension.
One nice thing about Kotlin Coroutines, there are libraries with basic implementations, but actually it is an language API to design suspending operations, you can use any execution model behind it, heavy threads, lightweight threads, reuse your engine loop system, it doesn't matter, it all will look like sync procedural code if you want. Or Actors, Channels, Generators.

Often people ask me why I like Kotlin Coroutines so much and dropped RxJava totally, instead of using them together, and my answer it's mostly that Rx is hard to read and reason, there's cognitive overload every time you need to read it. Doesn't matter if you know it well, you will always be thinking about the execution, while with suspending async model, I will just read the business logic line by line, as usual. I found it reeeally easier for juniors to get into, even when mixing some little reactive, overall they were faster solving problems with less async bugs.

When I meet the suspending paradigm, I learnt how much I was missing sync "old style code" and didn't know.

I agree with you, too much reactiveness may be hurtful. But I still love it for UI bindings, better yet for bidirectional bindings. I guess it's all about balance Β―_(ツ)_/Β―

Collapse
 
samolenkov profile image
Stanislav Samolenkov • Edited
  1. The cost of thread is hugely eggxagerated.

  2. The modern server frameworks have graceful solutions to limit thread number and to use one thread for several parallel tasks.

ReactiveX cons:

  1. There are no methodologies to develop complex information systems based purely on Rx.

  2. The code is diveded by numerous lambdas and business logic is scattered across source files by small chunks. It's practically impossible to track particular use-case implementation across the sources.

  3. Given two above the eventual outcome of Rx development is chaotic development metaphor (see Steve McConnel).

  4. Thus it's not new or progressive but travelling in past times before OOAD of Booch, before GOF patterns, before RUP by Rational Rose and Three Friends. It's back to early 1990th.

Collapse
 
stealthmusic profile image
Jan Wedel

The cost of thread is hugely eggxagerated.

I admit, I did not do any testing on Desktop VMs recently. But, I was developing a platform for embedded clients running HotSpot VMs. We had around 200kB of memory available (not joking). So we needed to be very carful with any kind of memory usage. We actually removed all Strings in log messages and replaced them by constant numbers. Long story short, I measured the memory consumption of creating a thread (which we needed) and it was around 4kB. I did not dig deep into what it exactly consisted of but at that time it didn't matter.

So, do you have any solid figures for recent Desktop JVMs?

Collapse
 
samolenkov profile image
Stanislav Samolenkov

I worked on CORBA framework in the beginning of 2000th and then 1000 threads on server was no issue. All pros for reactivex are artificial. There are a lot of solutions how to reschedule the thread waiting on asychronous call for the other task. The task is made async and called from scheduler. The idea is the same as OS schedules processes and threads on the same CPU.

Collapse
 
siy profile image
Sergiy Yevtushenko • Edited

First of all I see in comments great confusion between Rx* style of reactive programming and reactive programming in general. In fact there are two styles of reactive programming: one based on concept of stream (Rx*) and one based on concept of callback (in Java it's implemented in Vert.x). These are different styles and each has it's own properties and issues.

As correctly mentioned, Rx* style is not always convenient and enforcing "everything is stream, even single element" causes even more confusion.

The modified callback based approach - Promises, are much more convenient to use. While it still different from traditional synchronous code, it much closer and follows the same logical pattern - "when information is available do something".

As for why many threads are bad: the CPU is still limited in number of things it can do concurrently. Threads need to be scheduled and every context switch is expensive since it stops CPU pipeline. Traditional synchronous application is still asynchronous under the hood. Once thread is blocked, OS adds context to the group of waiting and switches to another task. Once operation is done, thread is added to pool of ready to run. All these switching is the root of poor performance and even worse scalability of synchronous code. By writing application in reactive way, we shift all these switching to user space, avoid unnecessary context changes and get far better performance and scalability.

Collapse
 
starodubtsevss profile image
Sergey • Edited

I worked with rxjava and vert.x for one year (before with scala and futures for 2 y.) before not much reactive, and may say: the futures case is middle ground if one wants to find a silver bullet.

So I agree with Jan. We got quite a lot of troubles with RX: learning curve is one thing.

As long as you play with it on workbench all is beautiful and shiny, in reality when whole team working with it, with intensive code reviews one year is not enough, assuming that rx part is one of several parts / microservice (other parts are not rx) - the mental effort to keep all parts by one team on track is not worth it.

I do not talk about vert.x cluster yet and rx part on ui It's different topic.

I belive in 95% cases the rx style is overkill.

Even it one definitely needs one: make sure only one team works on it : they would think only in the nail concepts and be happy with it.

Collapse
 
stealthmusic profile image
Jan Wedel

When you’re working with requests and responses, reactive call chains like map eg are just the wrong level of abstraction at that point. It exposes too much technology where you actually need a functional domain abstraction.

Collapse
 
yuriykulikov profile image
Yuriy Kulikov

As any other technology, reactive has to be taken with a grain of salt. I am currently working on a project where reactive was a great fit (a lot of persistent data streams, e.g. sensor data). However, I would consider other choices for a less "stream"-heavy application (like Kotlin koroutines).

Collapse
 
stealthmusic profile image
Jan Wedel

Getting loads of sensor data is definitively a good fit for reactive applications, but make sure you decouple persisting and enriching with master data to another service of possible.

Collapse
 
jaymeedwards profile image
Jayme Edwards πŸƒπŸ’»

Great take. It’s definitely the wrong tool for many products.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.