In recent years, the concept of micro frontends has emerged in the frontend development world. But what are micro frontends? Where do they come from?
In this article we will analyze the issues we faced in developing Mia-Platform Console and how the use of micro frontends has helped us to govern them.
We will approach this journey starting from a better known and well-established world: microservices on the backend side. We will see how the problems that the monolith architecture brings are similar to the problems we also face working with the frontends. Therefore, we will see how it is possible for the frontend to "follow the footsteps" of the work carried out on the backend side with the microservices architecture. After discussing about the problems and possible solutions, we will retrace the steps we took at Mia-Platform in using this architecture: the first tests, the subsequent obstacles, and their resolution.
From monolith to microservices
A web application generally offers different features that can be provided on the same page or have dedicated pages. Typically a website is developed as a single codebase and as a single application as regards the frontend.
This architecture has already been defined as the so-called "monoliths" on the backend. The series of problems that arise with the projects developed in monoliths has led to the birth of the nowadays famous microservices architecture.
The problems faced with the monoliths on the frontend side are similar to the backend side ones.
Let's see what are the disadvantages of using a monolithic architecture:
- Large and uneven codebase. Each developer has their own style and way of developing, which risk creating a codebase that is too difficult to maintain as the number of developers grows. As for the style, it is possible to mitigate it through static analysis of the code (like a linter), even if it is difficult to have a uniform code anyway. Strong pull request control will always be required in order to maintain a homogeneous codebase. Furthermore, a source code that is not homogeneous on the development patterns can make it more difficult to read and understand the code. An example of inhomogeneity can be for example "functional vs objects" (analogous to the "functional vs class component" in React).
- Knowledge of your own codebase. On a very large codebase there are likely to be pieces of code that are less known. If the code has not been developed following good practices in order to isolate individual features, and if you need to add a new feature, you need to get your hands on some code you have little confidence with.
- Interdependencies for feature releases. Having all the features in a single codebase makes it difficult to release new features as soon as they are ready. In fact, the release of a service may be blocked by the fact that another team is working on another feature, and the service is not releasable as it is unstable. This can be avoided by resorting to Feature Toggles, which allow you to keep the old behavior code. However, their use could be very expensive or difficult in some cases - such as when the functionality under Feature Toggle has particular side-effects, such as changing a configuration.
- Features scalability. Since multiple features coexist on the same service, with feasible different performance needs, it is impossible to scale them independently. In fact, you can choose to scale the entire service but not the single functionality.
Divide et impera
These issues have led to the definition of what we now call a microservice architecture.
The cardinal principle of a microservices architecture is the individual responsibility principle. The monolith is divided into areas of responsibility - ideally one area deals with only one business function - and each area is associated with a specific microservice.
An example of microservices architecture is Mia-Platform Console, which has the service that deals with the deployment, the one that deals with managing the configuration of the project, the one that deals with providing the runtime data of its namespace, etc. Each service is responsible for managing a single feature which, together with the features of the other services, builds up the entire application.
An architecture of this type allows you to architecturally isolate the various features, to scale them individually according to your needs, and to have reusable microservices (for example, a microservice that manages the payment can be reused in different business contexts). Furthermore, it allows you to improve performance, as each module can be developed with the most performing programming language for the tasks it has to perform.
In recent years, the evolution of web applications, alongside with the increasing frontend complexity, has led to the development of an approach similar to the microservices architecture one. This is how micro frontends are born.
The idea is to divide your project into several parts, based on functionality, and implement each of them in a dedicated web application. In order to show the frontends to the user in a single website, a frontend is used as a container, which will have the role of orchestrator for the visualization of these micro frontends.
Let's see how this type of architecture solves the problems we talked about previously:
- Large and uneven codebase. By dividing the project into multiple applications, you will have different codebases, each of which will be smaller as it will contain only a part of the functionality of the total project. Being smaller and focusing on some specific features, there will also tend to be fewer developers working on the single codebase. This leads to greater simplicity of code review and better organization of the codebase.
- Knowledge of the codebase. As the single service is smaller, and the codebase focuses on a specific feature, developers are more likely to know almost all of the service, which deals with the functionality they are responsible for.
The product team can be divided into Feature Teams, each of which deals with specific functionalities, and is therefore responsible for specific codebases. At Mia-Platform, for example, we have different teams working on the product, each of which is assigned specific functions of the Mia-Platform Console: Marketplace, Dashboard, Dev Portal, Fast Data, Flow Manager, etc.
- Interdependencies for feature releases. Since different features are carried out by different and independent services (each with its own version), it is possible to release the individual features when they are ready, avoiding the waiting queue of other features development. At Mia‑Platform this allows us, for example, to release a fix on the Deploy area while working on other areas of the Console: this is possible because the Deploy area is a separate micro frontend. The Feature Toggles can however be useful for managing the development of some "cross-area" features or those which are expected to be developed in the medium-long term.
Our approach to micro fronted
iframe: the first choice
The first step we took on Mia-Platform Console towards a micro frontend architecture was to use iframes, an HTML tag that we used in order to embed frontends into the console with new features.
We made sure to show an iframe on a specific path with the url on which the micro frontend we wanted to render is exposed.
The use of iframes, although fast to implement, presents an increase in complexity when it is necessary to manage communication, such as the passage of data, to a micro frontend. In fact, although each micro frontend deals with a specific functionality, in a real context it is often necessary to communicate among different parts of the application. For example, it may happen that you need to show the information recorded by another micro frontend into a section (in an e- commerce you may want to show on the detail page of an item that it has already been added to the cart).
The communication, depending on how the application was designed, can be managed through a server (example: socket or persistent data on a database) or entirely on the client side.
In the case of Mia-Platform Console, which was born as a monolith as regards the frontend, the communication between the different areas has always been managed on the frontend side. This approach has been maintained for communication between micro frontends.
The communication can take place in two directions. The micro frontend can communicate to the information container (for example that a change has been made to a system and therefore the configuration of the Console has changed), or the communication takes place from the container to the micro frontend in the case where the micro frontend needs data for which management it is not responsible (for example a list of data stored in the container).
Although it is still possible to communicate via iframe, the complexity of using this tool for communication between micro frontends has prompted us to look for alternative solutions.
There are several solutions to manage micro frontends without using iframes: Webpack Module Federation, Single SPA, Qiankun and many others.
We have chosen to use Qiankun, which is based on Single SPA.
The choice of using Qiankun
Qiankun takes care of providing functions (lifecycle) to be used to mount or dismount micro frontends in an application container. These functions will then be invoked by the library itself as needed: if the micro frontend is to be shown, then it will invoke the function to mount it, if it is not to be shown it will invoke the one to unmount it. The decision to show it or not is determined by rules that must be provided when taking a census of the list of micro frontends that the library must orchestrate.
The needs of communication among micro frontends in Mia-Platform Console
In Mia-Platform Console we needed to insert a communication between micro frontends when we first introduced Fast Data micro frontends in the Design area.
Previously we used micro frontends only at the level of main sections (Deploy, Monitoring, Runtime, etc.), which do not share any application state. The Design area, on the other hand, contains various subsections (microservices, endpoints, fast data, ...), which manage part of the data related to the configuration of the project that the user can modify.
When we added the Fast Data area, we needed to ensure that when the configuration save button was clicked, its data was also saved. We therefore needed a communication from the Fast Data micro frontend to the container.
The first approach we chose was to use the browser window, which is also shared with the micro frontend, to share an object that provides a common API interface between the container and the micro frontend. This API consists of a series of functions, such as an "update" function that allows the micro frontend to send the configuration of its updated data to the container, which then takes care of storing it and then sending it to the backend when the user presses the save button.
We subsequently encountered a similar need in the Flow Manager area. We have chosen to look for a more standard approach in order to manage communication without using the browser window. The choice fell on rxjs, a library that allows you to develop reactive applications that respond to the occurrence of certain events, through the Observer pattern. There is an "observed" object, called Subject and "observers" objects, called Observers, that are listening to changes in the state of the Subject. When the latter invokes an update function, observers are notified of the data change.
In our case we had the micro frontend that subscribed to the container (which instantiated the observed object).
We have subsequently implemented this communication architecture also in the opposite direction. In fact, in order to manage the visibility of the selection of the environment on which to deploy - based on the section in which you are located (deploy, history of the deployment) - we had to introduce a communication from the micro frontend to the container. Therefore, there is the container that subscribes to the micro frontend. When the micro frontend changes section (for example, you go to the History subsection of the Deploy page) the container is notified of the change, and it will take care of hiding the environment selection.
Many services, many resources
Since on Mia-Platform Console we do not use any CDN for the storage of static frontend files, the use of a micro frontend architecture has brought with it an increase in the resources required for its operation. In fact, for each micro frontend we had to create a service that would expose it in order to allow Qiankun to obtain the static files of the micro frontend.
On Mia-Platform Console we therefore needed 10 microservices in order to serve all the micro frontends that make it up. Having 10 microservices means having at least 10 pods at runtime on your namespace, which therefore require a certain amount of resources.
In order to decrease the consumption of resources and the amount of services at runtime, we have decided to aggregate all the micro frontends into a single microservice, which takes care of serving each of them on a specific path. In this way, on the frontend side we have maintained the micro frontend architecture. But on the runtime side, the micro frontends are all served by the same pod of the cluster.
This allowed us to reduce from 10 to 1 the number of microservices on the cluster, needed for the frontend of Mia-Platform Console.
Starting from our experience we have released micro-lc, the solution for micro frontend orchestration that simplifies the management of micro frontends through an application.
micro-lc encompasses all the backend and frontend parts, and allows them to be extended via plugins, necessary to develop frontend applications, thus speeding up the time-to-market of new solutions.
The project is open-source and available on GitHub.
The increasing complexity of frontends in modern web applications has brought with it progressively interesting engineering challenges. In recent years, the number of web frameworks such as React, Angular, Vue.js and many others have increased, each one with its own peculiarities and strengths.
Websites have gone from being the simple static pages of the 90s - with the sole purpose of showing information and with all the complexity managed on the backend side - to being real web applications with a strong business logic also on the frontend side.
The need to dominate this complexity has therefore become increasingly mandatory, both from a code point of view and from a work management point of view. The use of a micro frontend architecture is one of the answers to this complexity.
© MIA s.r.l. All rights reserved