Introduction to Reactive Programming
This article is the first one of a series focused on how to approach Reactive Programming in Java, what it is, what it is not, how it can be leveraged to increase your application performance and what are the most common pitfalls when using this approach coming from a “imperative/OOP” background.
In this first piece we’ll dive into the basics of this topic, answering two questions: “What is Reactive Programming?” and “Why should I care about it?”
Moving on with the series, the articles will have this common background theme of Reactive Programming, but we will look at different aspects of it, from debuggability of systems written following the Reactive Pattern, to observability of said systems, to how to properly design one.
These articles will start to paint a broader picture on this new Reactive paradigm, where we will see, with real world use case and examples, what difference it makes approaching problems from this angle, in terms of performance, system design and maintainability.
As always with any technology there are tradeoffs that come with it, and we need to have the knowledge to take a well-informed choice when it comes to designing and operating these systems.
The aim of this series is providing you, the reader, a framework of knowledge that you can use to assess and evaluate if this approach is the right one for you, what you gain by adopting it and where the complexity shifts in your system.
We wanted to give a pragmatic tone to this series, so you’ll find more examples (based on our experience and our use cases) than theory here, though, where required, we will give you all the required context and background to assess why we opted for one option instead of another. It’s also important to always remember that there is no Silver Bullet and no solution is always 100% right or wrong, but each one comes with varying degrees of maintainability, clarity and performance, so we always need to make a conscious effort to evaluate a solution in its entirety and see where it fits in the greater scheme of our application.
This is to say that sometimes things may not look so intuitive and there might be better ways to solve a problem, but these “better” solutions come with tradeoffs that don’t go along with the rest of the system and this would cause more problems (possibly in other parts of the system) than we would solve by adopting these.
Despite how trivial this might sound, evaluating a given solution first requires knowing what that solution actually is, what are the problems that this solution is supposed to solve, how this solution is able to solve these problems and where this solution gets something right that the previous (or alternative) ones fail.
In our case, to answer the questions posed before: the solution we are talking about is the Reactive paradigm, the problems it’s supposed to solve are related to scalability, flow control and performance, the way it’s able to solve this is by leveraging the use of non-blocking I/O, backpressure and efficient thread scheduling and what it gets right is that this approach is abstracted behind very convenient interfaces so that most of the complexity is shifted to the framework/library and the developer is relieved from having to take care of this.
Of course there is much more than this to discuss, but we cannot say everything at once, and since we have to start from somewhere, we will start from the very beginning of what we are dealing with and why we might want to know more about this, and we’ll follow (on the next articles) with some interesting aspects on this new Reactive paradigm.
Our Use Case at Itembase
Here at Itembase (https://www.itembase.com/) we built and offer an integration platform for eCommerce platforms.
In a nutshell, if you want to integrate with, let’s say, Shopify and Magento, you don’t have to interface directly with their APIs on your own and manage that, you can instead simply ask us to connect your application to these eCommerce platforms and we’ll take care of everything.
One more cool fact is that we deliver you a standardized payload that is always the same regardless of which eCommerce system you want to talk to. Because of this, you will only need to integrate with us once, to be able to interact with N eCommerce systems/platforms.
The reduction of the integration space for our customers, in terms of different platforms’ APIs, from O(N) to O(1), isn’t eliminating any complexity that was there before, that’s simply been shifted onto us, because we integrate with all these platform and manage everything that comes with it, like handling authentication, versioning, and so on.
This means that we need a way to tame this complexity while at the same time guaranteeing a good performance level and also making sure we can handle all the load that comes from high-volume eCommerce systems.
In addition to that, we also need to address one critical aspect of our customers’ integration: What if they cannot handle the load that comes from the eCommerce system?
This is not a trivial problem, because not everyone can handle tens of requests per minute (or even per second) and nor are they expected to, since we take care of making sure we don’t overwhelm their systems through a combination of rate limiting and backpressure.
We’re in between a (potentially) high throughput source (think Shopify) that can produce tens of records/webhooks per seconds and a low throughput sink (think a typical custom application deployed on a VPS) and we need to take care of not passing this load all at once otherwise we will undoubtedly crash the downstream system, our clients.
This constraint, among many others that we will look into in the next articles, was one of the key reasons why we decided to design our systems following the Reactive paradigm. With this approach, we don’t waste time in blocking I/O operations, we don’t spawn hundreds of threads if we serve a high number of connections and we can easily limit our throughput based on the downstream system we are sending our data to.
Coupling this Reactive approach with our microservices architecture allows us to scale horizontally and handle great amounts of traffic with little to no overhead on our servers, as our deployed applications are organized in set of microservices, with each one taking care of exactly one business concern.
It follows that each microservice has different load patterns (e.g. some might be I/O bound and some might be CPU bound), and through the use of backpressure (not only towards our clients but also towards our own internal systems) we can be sure that the fastest microservices don’t overwhelm the slower ones, because the downstream receivers will signal that they are near capacity limit and ask the upstream producers to slow down.
Designing and implementing systems according to the Reactive paradigm requires a shift in thinking about our approach to Software Engineering, because we are now moving on from “message passing between objects” as in the OOP case, to principles like “pipeline of functions that operate on a stream of data” and “pipeline composition of elementary operations to achieve high level processing operations”.
This shift in thinking and design helps writing more robust and testable code, because now we have to clearly separate our modules and classes into small independent operations (that are easily unit-testable) and compose them into high level pipelines, which reduce the overall complexity of the system.
The reason is that it’s easier to debug a complex business logic method if this is just a composition of simpler and low-level primitives, compared to the case where this method is just concentrating all the details inside itself, because in the latter case the context we need to keep in our head becomes much bigger and it takes more time and resources to properly follow the logic and find the root cause of a possible issue.
To summarize, we, at Itembase, have to design for a number of constraints that led us to evaluate this Reactive paradigm and after a learning curve, we managed to find the right spot where we are able to satisfy all the above-mentioned constraints and are comfortable with our current system.
Nonetheless, we still look for opportunities to improve our own systems and solutions and are always on the lookout for alternatives and approaches that can challenge, and hopefully expand, our understanding of this problem space, eCommerce integration, and part of its solution space, normalized data delivery to a variety of systems, all while solving some of the usual constraints like performance level and system stability and some of the unusual ones like downstream flow control, resilience in the face of a cascading failure and ability to replay data that has been received but lost in our downstream consumers (possibly due to their own issues).