Reactive Programming in the Web Application Context
Spring Webflux is the fifth release of the popular Java framework which introduces the model of Reactive Programming, with its non-blocking I/O and replacing classic Spring MVC which ran on the Servlets API.
The term Reactive Programming refers to a paradigm built around reacting to changes — network components reacting to I/O events, UI controllers reacting to GUI events, and so on.
Non-blocking code is reactive, because instead of being blocked, it’s now reacting to notifications such as operations complete or data available, for example.
The idea behind Reactive Programming is that we should be able to scale more efficiently in terms of concurrency while still using a small number of threads.
The usual approach of one thread per request does not scale very well beyond a certain number of concurrent requests, be it ten thousand (as in C10k) or one million.
The overhead of managing tens of thousands of threads is quite significant, and there’s also the challenge that if we perform blocking I/O code on a thread, that thread is stalled waiting for the operation to complete and cannot be used for anything else.
If we have an I/O bound application and we use a blocking I/O approach, we will certainly hit scaling issues trying to handle an increasing number of connections simultaneously.
These issues have classically (and rightfully) been solved via horizontal scaling (link) when the application was stateless enough that replicating it across a number of servers was not a concern.
But what if there was another way of handling I/O bound applications? Specificallyapplications written in Java?
The answer is non-blocking I/O using the primitives that the JVM offers, like java.nio.
Looking around, we find Netty, an NIO client server framework which enables quick and easy development of network applications, as they say on their website.
Fantastic! We have something we can start working with to convert all of our blocking code into this crazy fast non-blocking I/O, except that it’s not so easy.
Using java.nio is a bit inconvenient because it’s like using InputStream and OutputStream when working with blocking I/O, it’s a bit too low level. Sure, it can be done, but there’s some time that you need to spend in order to get it right.
Using Netty is also a good idea, except it’s also a bit low level in the sense that you have to handle channels, messages, buffers and so on, which, again, is doable, but not so convenient for the majority of applications.
Given the addition of java.nio primitives to the JVM (bringing non-blocking support for Java programs across all the OSes where the JVM runs) and the proliferation of low level non-blocking libraries, the Spring Core Team realized that there was a need to make these non-blocking APIs easier to use, and, above all, more convenient in terms of integration with the already-existing Spring applications.
Spring, as said above, with its fifth release, introduces Webflux, which is the web framework that brings Reactive Programming, through Project Reactor, to the mainstream Spring community.
There are a lot of changes (and related Spring Projects) that come with Webflux, some of them linked with the release of Java 8, with its support for Lambda functions.
Why are lambda functions relevant to Webflux, or to Reactive Programming in general?
The reason is that, usually, Reactive Programming is a very good candidate where the functional approach (from the model of Functional Programming) can be applied.
This combination led to the creation of Functional Reactive Programming (FRP) where the non-blocking I/O approach is coupled with the ideas of higher order functions, lambdas, method references and a whole world of data immutability and concurrency patterns.
Basically, it’s very easy to write Reactive code and make it functional.
If you were to ask ‘What are the main components of Reactive Programming?’, I would reply with the following chapter.
Two Words on Reactive Programming
Reacting to an event, even in the real world, just means that as soon as we become aware of something happening, we perform a number of actions that relate to the thing that just happened.
In the software world this translates very well to callbacks, which are just functions (to simplify) that get executed when something has happened, thus reacting to an event.
“So, in order to do Reactive Programming, I only have to use callbacks?” No, that’s a good start but there are other factors that need to be considered to be properly reactive in our code.
The other point is flow control, which just means having a system in place that allows us not to be overloaded by the flood of events that come in too fast.
Imagine if your application suddenly received a billion requests per second; it would probably crash as there is no way to process that amount of incoming data.
Having a form of flow control (called backpressure in the reactive lingo) allows our application to request the precise amount of data we can handle, and not accepting any surplus that would cause us problems.
One last analogy needed before we can fully define Reactive Programming relates to producers and consumers.
Simplifying the concept, our applications are boxes which exchange data between themselves in the form of messages, and we can designate one (or more) application(s) as a producer and one (or more) application(s) as consumer.
Analyzing our system through the lenses of Producer/Consumer makes it easier to see the advantages of backpressure.
For instance, imagine one system that consumes all the tweets that can be retrieved from Twitter API for a given hashtag.
It’s very likely that our application has a smaller scale than the whole Twitter API infrastructure, and thus cannot consume that many events, let’s say 100 tweet events per second.
If we tried to ingest all those tweets at the same time, we would crash the application because the load is greater than our capacity.
We can then model Twitter APIs as a producer of messages of type Tweet-Event and our application as the consumer.
The idea of backpressure is that we ask Twitter to notify us of at most N events, effectively signaling to our producer that he may be producing too fast and that we can process only up to N events in a given timeframe.
This allows our application to gracefully handle the load without crashing when there’s a spike in the number of events.
So, to be reactive, basically we would need to have asynchronous callbacks that implement backpressure, modeled as producers and consumers.
These reactive producers and consumers are provided to use by Project Reactor as Mono and Flux, wrapper types to indicate non-blocking, backpressure sequences of elements.
In Project Reactor, Producers are called Publishers and Consumers are called Subscribers, but the same definitions and ideas apply.
The core idea is that we build a pipeline of data publishers that we can operate on using one or more subscribers. Contrary to the usual approach of blocking programming style where we tell the code what to do, when and where (in which thread), with the reactive approach we describe a series of operations that should be performed on the data that is created by any publisher and consumed by every subscriber.
It is called reactive pipeline because it looks like that, a series of steps that each describe a transformation or an operation performed on the data that passes through those steps.
Reactive publishers are collections provided by Project Reactor that support reactive operations, such as filter, map, flatmap, etc.
These collections expose an API that allows the developer to build pipelines that operates on said publishers and also allows consumers (subscribers) to apply backpressure if the producer is emitting data too fast.
Even if you are not familiar with Functional Programming or Reactive Programming or with the non-blocking I/O stuff, we can start with an example:
Knowing that Flux<T> stands for “a reactive collection of one or more elements of type T”, we can see that:
- We can define a data source from where our Publisher will get the data to publish (in this case our Flux<Integer>). It could be anything, an HTTP socket listener, a File Reader, in this case it’s a simple publisher of Integers initialized statically (with the just operator).
- We can define the list of steps we want to perform and assign these steps to a variable of the same type returned from this pipeline. Basically, we are abstracting the idea of a sequence of operations being performed in a given order to some data.
The variable pipeline does not execute any code on dataSource, it instead describes what happens if we execute it. This is the important shift in thinking about reactive.
There is a disconnect between where we define the operations on the code, and where (and when) they are executed.
- With the .subscribe() method we tell the pipeline that we are ready to perform these steps and now they are getting executed. For the sake of this example the execution of these steps follows the definition of the reactive pipeline, but of course we could have just executed this far away from where it was defined.
- Nothing prevents us from re-using this pipeline on another publisher, as long as it is of type Integer. It’s important to mention that we cannot reuse the same publisher more than once because once data is emitted, there is no way to get it back (unless in very specific cases we are not going to discuss now), so remember we cannot reuse the same publisher (Mono/Flux) and expect the same elements to be present after draining it.
- The functions specified inside the .filter() and .map() operators are called lambda functions, and basically are anonymous functions (they don’t have a name) that are defined and used right on the spot where they are needed, and are not available outside of that scope.
The .filter() operator requires a function (as its argument) with the signature boolean func(Integer) (a function that takes an Integer and returns a boolean).
The lambda function passed to it, does exactly that, takes an Integer (named num, could be any name) and returns a boolean, as required from the .filter() operator, it’s just that instead of creating an instance of a class Predicate (it’s the type of class that contains just a function that takes a value [an Integer in this case] and returns a boolean), we decided to pass a lambda, which makes it quicker and easier to understand.
The part before the -> is the parameter list and the part after is the return statement, with the keyword return removed for clarity.
For a more thorough and in-depth explanation of lambda functions, please have a look at their documentation.
- In order to execute the pipeline, we need that someone subscribes to it, otherwise there’s no point in producing and transforming data if no one will ever consume it.
This is an advantage, because it allows us to define pipelines in one place and use them in a completely different method, class, package or module.
This is in line with patterns of programming where operations are not performed until there’s a necessity for them to be performed, and in the Reactive World, it makes sense to react to something only if our reaction will be seen by at least someone, in this case our subscribers.
So, if we define a pipeline, provide it with a datasource, but no one subscribes to this pipeline, no operation of that pipeline will be performed.
This is very important, in the Reactive World of Project Reactor, nothing happens to a pipeline until someone subscribes to it.
Imagine a reactive pipeline, a series of operations on publishers performed by consumers, that is composed of a data-source and three operations.
These three operations can be whatever, filter, map, flatmap, extend, and so on.
Assuming a generic operator, the code might look like this:
Generic types and operations should not distract us from the point, which is that this pipeline has been created right now, but will never execute.
No one is subscribing to this pipeline and the three operations will never be executed because no one is signaling his intention to consume the data produced by this pipeline.
The reason is that the answer to the question “Would you perform work if you knew, with 100% accuracy, that no one will ever see or use that work?” is negative.
No one wants to perform any work if they know that their output will never, ever, be seen or used by someone.
Here the phrase “to see the result of an operation” is used with the intended meaning that code related to each operation might produce side effects (printing to screen for example).
So, in order to optimize the performance of the software and make sure that we don’t ever perform work if it’s not needed, the pipeline will not execute until someone subscribes to it.
Without this policy of not doing anything unless it’s strictly necessary, in the case that no one subscribed to a reactive pipeline, what would happen is that after executing (potentially very expensive) work on that data in the pipeline, the result should simply be discarded because no one will consume it.
Does this feel like a smart decision to you? Performing work whose output you know for sure you have to discard?
In order to make our pipeline actually do some work and execute the steps we defined earlier, we need to subscribe to it, and one way of doing that is by calling the .subscribe() operator at the end of the pipeline, like this:
So, with this introduction on Reactive Programming out of the way, let’s dive a bit deeper into the Project Reactor types and what we can do with them.