{"id":987991,"date":"2020-07-07T12:00:00","date_gmt":"2020-07-07T12:00:00","guid":{"rendered":"https:\/\/woocommerce-593090-1918669.cloudwaysapps.com\/?p=987991"},"modified":"2021-05-17T10:58:01","modified_gmt":"2021-05-17T10:58:01","slug":"spring-boot-2-best-practices-for-reactive-web-applications","status":"publish","type":"post","link":"https:\/\/woocommerce-593090-1918669.cloudwaysapps.com\/spring-boot-2-best-practices-for-reactive-web-applications","title":{"rendered":"Spring Boot 2: Best Practices for Reactive Web Applications"},"content":{"rendered":"
In this article we will highlight some of the best practices when writing a Reactive Application, and more specifically a Spring Webflux application in the context of Spring Boot 2. Each best practice suggested will have a rationale accompanying it that shows when it’s appropriate to use and when it’s not, describing the most common use cases for it. We will close with a look at what are the most common concepts to keep in mind when writing a Reactive Application.<\/p>\n
<\/p>\n
Let’s start by saying that no two applications are equal, and thus there is no one set of practices that will work 100% of the time for every possible application.<\/p>\n
What we can do is provide a reasonable list of guidelines and practices derived from the collective development experience of the industry and invite the reader to apply his best judgement when evaluating each one of them, suggesting the question “Does it make sense to incorporate one or more of this into the current application workflow? Is the codebase disrupted too much?”.<\/p>\n
Sometimes the answer to those question make it very difficult to adopt any best practice, and for a variety of different reasons, it’s not possible to change the way the codebase is structured and working at the present moment.<\/p>\n
Sometimes it’s possible to work on a greenfield project and thus there’s more freedom when it comes to incorporating the suggestions mentioned below, which is the ideal scenario, because there is no pre-existing “scaffolding” that will render this adaptation difficult.<\/p>\n
The most important thing to always keep in mind is the consistency of the codebase and the fact that any change comes with a set of trade-offs that need to be thoroughly understood before implementing them.<\/p>\n
While it’s true that some of the best practices will be very simple (bordering on triviality), some of them will bring a more profound impact to the application, both from an architectural point of view and also from a debuggability point of view, which makes them the most “prolific” but also the most impactful.<\/p>\n
A codebase that is full of best practices but is inconsistent in style, architecture and code\/module organization is not going to be pleasant to work with, and while it might be performant enough to scale, it is surely not the best way to achieve that result.<\/p>\n
Consistency here in this context means simply that there are some agreed-upon rules that the developers will follow when writing the code, so it is always organized the same way and also, when there are exceptions, those need to be clearly documented and justified.<\/p>\n
As an example, it can be established that every reactive component is written as an event emitter and\/or an event subscriber, so the entire architecture of the application will rely on event broadcasting and subscription.<\/p>\n
This means that there will be a need for some sort of event persistence, and thus maybe an external system will be involved (such as Kafka, RabbitMQ, ZeroMQ etc.) that brings additional concerns and constraints into the design of the system.<\/p>\nWith this rule that’s been decided and agreed-upon, developers can now start creating modules and components following this model, which means that they will “shape” the codebase in a certain way according to this event-driven direction decided in the design phase.
\nIntroducing some best practices that will disrupt this architecture is not a wise choice, even if those best practices might be needed or well-suited for the problem at hand.\n
Creating inconsistencies at such deep level like the architecture of an application is bound to disrupt the entire codebase, and potentially expose code paths that were not meant to be exposed, along with some bugs that might have not been manifested before if not for this change that’s just been introduced.<\/p>\n
What this implies is that it’s necessary to understand when it’s worth to introduce some best practices that are quite impactful on the codebase and when it’s better to leave things as they already are, even if they are not the best but still good enough.<\/p>\n
This judgement can only come from experience, from the intimate knowledge of the application and from a substantial amount of time spent evaluating what it means changing the code to introduce a potentially disruptive change.<\/p>\n
Studying and understanding what we are working on is always time well spent, and it can only lead to a more complete development experience and, more importantly, to a better capability of future-proofing our code against changes that might come when requirements will inevitably change.<\/p>\n
Some situations, by their very nature, are not a good environment for introducing new changes like adopting some best practices or rewriting some components to fit a better architectural model.<\/p>\n
These situations might be an incoming deadline, a project that’s being built as a prototype just to understand the problem domain and possibly the refactoring of a legacy system, which are very delicate and are not the right place and time to introduce new code that we might not be familiar with.<\/p>\n
In summary, try to understand each point discussed below, asking yourself if your codebase is ready to change in the way that’s suggested in that best practice, and if yes, what might be the impact on the overall architecture of the application.<\/p>\n
<\/p>\n
With the introduction on the topic out of the way, we can now proceed to discuss the best practices that might be useful when writing a Reactive Application, with a focus on Spring Boot 2 based codebases.<\/p>\n\n
What this means is that we should try to avoid subscribing directly to a Reactive Stream in our code, because this operation is a low-level one that’s exposed for the developer’s convenience where it’s not possible to do otherwise, while we should instead focus our reactive code on the high-level concerns and shouldn’t care about these kind of details.<\/p>\n
When we use the raw “subscribe()” method or any of its overloaded variants, we are concerning ourselves with handling data buffering, backpressure and outbound flow control, which is something we should not care about in the vast majority of cases.<\/p>\n
If possible, and with Spring Webflux it always is, try to use a library that handles the subscription process for you, freeing yourself from the need of writing boilerplate code as much as possible<\/p>\n
This is a very impactful change, and thus might not be applicable everywhere, but the gist of it is to abstract away (behind well-defined interfaces) the “ugliness” of the external world, and write code that is free from such side effects, that will always produce the same result given the same combination of input and external state.<\/p>\n
What this means is that our business logic core classes should not directly interact with the external world, but instead relay on specialized classes that feed data into these components and will eventually handle the writeback\/sending\/output of the processed data that’s been obtained.<\/p>\n
While this might sound trivial, it’s not an easy feat and requires a lot of discipline when designing the system, because the modules need to be strictly separated via interfaces and the implementation details need to be abstracted away, so it’s always possible to swap one implementation for another and nothing will change, which is very useful for testing purposes too.<\/p>\n
Modelling and concentrating all the state from the outside world into a number of POJOs will allow us to write business logic that takes in this external state objects and will work on that, always producing the same result if given the same input and the same external state objects, which is a great property to have in a Reactive System due to the fact that immutable and pure systems naturally compose very well with the functional and reactive approach that we want to adopt.<\/p>\n
Another added benefit is that we will simplify testing a lot because we can craft a particular external state “view of the world” as objects and pass those to our business components, checking if they behave like we expect them to or if there are edge cases we might have not considered.<\/p>\n
How to practically do this is quite complex and requires first-hand knowledge of the system, but the general advice is to separate the types of data that’s needed to perform a given business function and model each of those types into POJOs\/interfaces according to their nature, if they are a required input for the system of if they represent external state.<\/p>\n
As an example, we might have a business need to calculate the tax due on an item and write that back into a database, that we solved by creating an interface and implementing its method “void calculateTax(Long itemId, BigDecimal vatRate)”.<\/p>\n
How we might have solved this is by getting the item from the database, calculate the tax rate based on the input VAT and write it back to the Database, all of it in the same method.<\/p>\n
There is nothing wrong with that per-se, it will work 99% of the time there’s not an issue with the DB and it will suffice for most applications, even if it might be a bit too much for a single method.<\/p>\n
In the Reactive Model, we must first not perform any blocking calls, which already rules out the possibility of including the DB query\/methods into this method that we will use in a Reactive Pipeline, and furthermore, this is not pure, because it has side effects and relies on\u00a0implicit state<\/strong>\u00a0represented by the Database (even if we pass the same vatRate, we might end up with different results because the product at that ID changed, for example, and this is not a pure function).<\/p>\n If we want to adopt the best practice suggested above, we should decompose our method into a set of three methods, chained in a Reactive Pipeline and make sure we “wrap” our DB calls into the appropriate “publishOn” calls to make them non-blocking (or even better start using a reactive JDBC driver where applicable).<\/p>\n The methods might look like this:<\/p>\n“Mono<Item> fetchItem(Long itemId)”
\n“Mono<BigDecimal> calculateTax(Mono<Item> item, BigDecimal vatRate)”
\n“Mono<Void> writeTaxToDB(Mono<BigDecimal> taxAmount)”\n