TSM - Migrating frontend functionalities from monolith to microfrontends

Vlad Luca - Senior Frontend Developer @ PitechPlus

Migrating frontend functionalities from monolith to microfrontends

A requirement that most of us have probably tackled already, or if not, most likely we will have in the future, is the migration of existing functionalities to a new and modern technology. Although there are multiple reasons why such a migration can be beneficial, we will try to expose those that have been most relevant to us, but first let's see which was the initial context. In our case, the goal was to migrate the existing functionalities from a monolithic application to microservices on the backend side and to microfrontends on the frontend side. Our objective for this article is to present the challenges encountered along the way and at the same time the solutions we have chosen.

If we were to explain as simply as possible the concept of "monolith application", it is a software application in which the frontend and backend functionalities are developed in the same codebase. In our case we start from a Model - View - Controller (MVC) application written in PHP with Laravel framework, the frontend part being implemented with blade templates and jQuery. The diagram below shows the structure of a classic monolith application.

What does the term microfrontend stands for

The concept of microservice is one that most of us are familiar with but when it comes to microfrontend things are a bit different. The idea of ​​microfrontend has started to gain strength in recent years, but it is not yet widely used, practically this concept is aiming to bring the benefits associated with microservices in a frontend application. Lately, the most common way to develop frontend applications is to build a single page application (SPA), developed by a dedicated frontend team. Problems that can occur if we use this approach are generally related to the size of the codebase which can become difficult to maintain. Such an application can be called a "frontend monolith". If we look at our original purpose, in addition to the fact that we want to use modern technology, we also want to move away from the notion of monolith. One thing that we have not specified so far and that lies behind many technical decisions that we have made, is the fact that our team consists of about 100 programmers who, until the moment we started to think about rewriting the functionalities, were working simultaneously on the same codebase. The principle behind microfrontends is how the application is organized, each team being responsible for a subdomain of the application business. Other ideas underlying this approach refer to the stack of technologies used for development, with virtually each team having the flexibility to decide what and how to use without the need for prior coordination with the other teams involved in the project. Each team has to develop independent applications, both on the backend and on the frontend side, applications that are not based for example on global variables, shared states, etc. In the figure below we can see the way teams are organized when working with microfrontends and how the components are interacting with each other.

Why we needed this migration

One of the main arguments in favor of migrating from a monolithic codebase to a microfrontend application is the existence of old dependencies that are no longer actively maintained. Among these dependencies we can, without a doubt, include the jQuery library. We can say that the use of jQuery is indicated for small projects that do not require complex manipulation of the DOM or that do not have a complex functionality so that no heavy processing is required. In our case, the project grew in an exponential way, and together with it the difficulty of the developed functionalities, That being the reason for which we started to feel the limitations of the existing technological stack. Thus, the migration to microfrontends based on a JavaScript framework, React in our case, helps us to eliminate the legacy code and at the same time offers us many more possibilities for the development of new functionalities, as React enjoys a growing popularity and a very active community. If we analyze the following graph, we can see very clearly the tendency of users to give up jQuery as a dependency in their projects. The graph represents the popularity of Google searches for the terms "jQuery" and "React", the value 100 representing the maximum popularity of the term, and the value 50 showing that the popularity has decreased by half of the maximum recorded.

Another reason why the migration from monolith to microfrontend applications can be beneficial is the difficulty involved in maintaining a monolithic codebase. If several teams are working on the project, there is a high risk of duplicate code because the design specifications are most likely the same for all teams, making it very difficult to identify the existence of common elements already developed so that they can be reused. In some cases, even if such a common element is available, the way the code is organized may make it impossible to use without duplication.

Basically, by moving one or more functionalities in one or more microfrontends, we make sure that each microfrontend has its own responsibility for a certain part of the application, operating independently of the others, thus facilitating maintenance. In our case, another element that has proven to be helpful is the development of a reusable components library that can be used by all teams in their microfrontends. The respective library includes components such as: dropdowns, form fields, popups and other components necessary to implement the functionalities within several teams.

As a monolithic application usually has a large number of functionalities, ideally they must be covered by functional tests, but in our case they were completely missing, which is a risk especially in situations where the application is in a continuous process of development. For us, the migration to a modern technology meant a very good opportunity for the implementation of functional tests and together with them we gain an increased level of trust and safety within the team.

Migration workflow

The first problem we encountered even before deciding which technological stack to migrate to was finding a workflow that would meet our needs. Because the application we are working on is live and has tens of thousands of active users each day, we are aware that, in addition to migrating features, we will still need to provide maintenance and this will not allow us to allocate the entire team effort only on migration tasks. Along with the rewriting of the functionalities, it was necessary to modify the interface design and sometimes the whole functionality in order to satisfy the users needs as well as possible. This led to the decision to use feature flags for releases so that changes to the live platform could be made gradually, initially only to some users, so that we could collect feedback and fix possible issues if they appear before release for 100% of users. Another important element in the migration process was the definition of the order in which the functionalities are to be migrated, the final goal being the migration of 100% of the code. As mentioned before, in our case it was not possible to allocate the entire team just to rewrite the features, which is why we decided to start progressively by implementing small functional and independent parts of a page or identifying common elements that appear on several pages and the development of reusable components, which were later to be included in the monolith.

The inclusion of a microfrontend component in the monolith consisted in adding a div element in the HTML of the page we need, a div on which, if needed, we added data-attributes to make available to the microfrontend some data that at that time was only accessible from the monolith. This way of transmitting data is temporary but at the same time it is necessary because it helps us to disconnect the frontend from the implementation of microservices on the backend. For example, in the image below we have a div with the id "microfrontend-main-container" which will be used to identify the element in which the React component in the microfrontend is to be rendered.

<div id="microfrontend-main-container"
     data-position-name="{{$positionName}}"
     data-language="{{$currentLocale}}"/>

How does it work?

Wondering how the microfrontend decides what to render in the divs included in the monolith files? During build, the microfrontend generates a JavaScript file and a CSS file that needs to be included in the monolith, in the files where we need the components from the microfrontend. Basically, in our case in the blade files where we have to include the microfrontend, the following scripts must be added:

<link rel="stylesheet" href="{{$mfeCss}}">
<script src={{$mfeJs}}></script>

Thus, the files generated by the microfrontend are included in the pages and we are one step closer to having the components rendered from the microfrontend to the monolith. In our situation, the files that are generated in the build from the microfrontend are uploaded to the cloud and served through a microservice by the monolith, and then they are included in the pages according to the scripts previously presented.

In order for the microfrontend to decide what to render in a certain div, it uses an event-based system, practically from the monolith we dispatch a custom event that contains a key based on which the microfrontend, which implements the event listener, renders the corresponding component. For example, we may have situations where the message received by the microfrontend indicates that a certain component needs to be rendered if the page is not fully migrated or the situation where we have a generic message and the microfrontend will render the components based on the current route.

Conclusion

Perhaps the biggest benefit of this migration, for us as a team, is the level of trust we have in the developed code. Starting with a codebase that we did not know and that did not have any functional tests, we were at the point where any change meant a high risk of regressions, no matter how well we managed to test our work. At the same time, the possibility of our changes impacting some features developed by other teams, on which we have no visibility, was high. By migrating to microfrontends and microservices we have managed to get to the point where each team that works on the project is independent, and this is a very important aspect especially when the number of teams is considerable and is constantly growing. Another great benefit is that we managed to eliminate outdated libraries from the project, which were an impediment every time it was necessary to develop new features. The use of up-to-date technology, which is constantly maintained and has an active community, offers us multiple possibilities in implementing new functionalities, which is an important factor when it comes to the satisfaction of team members.

Another aspect, as important as those already mentioned, is the degree of customer satisfaction that now owns a much more reliable application, the rate of reported bugs being in a continuous decline, directly proportional to the number of features that we manage to migrate, and perhaps most importantly, the client has an application that is much easier to develop in the future, having no technical constraints.