As a Software Architect, managing a software architecture that evolves to adapt to the business over the years can be a complex task. At the beginning of its life, the software architecture was scaling low and could be managed by one Back-End and one Front-End team as well as it consisted only of three layers: Single Page Application, Application Server and Database.
As the number of users and services increases, the need for a highly scalable architecture arises. At the same time, the old architecture evolves into Microservices which makes our application scalable in terms of development. In fact, each Microservices is assigned to different Back-End teams in charge of managing it with the advantage to choose and adopt autonomous releases and technologies. At this stage, the architecture is ready to add new functionalities while for the Front-End team it is difficult to manage it saving time and work.
That’s when Microfrontends come into play. But what are exactly MFEs?
MFEs are technical and logical representation of a business subdomain in Domain-Driven Design (DDD) which allows for independent development and release of application parts. This approach avoids shared logic between subdomains and enables management by a single team.
Very important concept for applying this approach is that our architecture is domain-driven and not vice versa, so our MFEs will represent a precise subdomain. Each subdomain will be developed by a single team that will be free in choosing the right technology and methodology to implement and manage the functionality. To ensure good management of teams' work, it is important to design an architecture with very low coupling between Microfrontends (MFEs). This approach allows for independent development and release experiences.
But how can we apply the architecture and MFE? It is crucial to consider the specific context in which you are working. As always, the business domain will play a significant role in guiding the architectural decisions. The key problems that need to be addressed will be the following: Definition; Composition and Routing; Communication.
There are two techniques to define and to effectively implement the Microfrontends (MFE) architecture: Horizontal Splitting and Vertical Splitting. Let's explore each technique:
- In Horizontal splitting, multiple MFEs are accommodated within a single view of the application. For example, a home page may consist of MFEs such as footer, header, article preview and article list. The main challenge with this approach is ensuring that teams can create a cohesive look and feel based on a common system design. However, there is a risk of creating strongly coupled components, which may deviate from the business-defined MFEs.
- In Vertical Splitting, the application is divided into logical units based on Business subdomains. For example, there may be separate MFEs for the login page, home page, and catalog page, each designed as if it were a separate Single Page Application (SPA). This technique allows teams to have in-depth knowledge about a subdomain and facilitates the implementation of system design.
Composition and routing
Composition and routing are critical aspects of implementing the Microfrontends (MFE) architecture. There are three techniques that can be used to compose MFEs and handle routing:
- Client-Side Composition: The app shell is responsible for merging MFEs and handling routing between them. The composition can be done at runtime or build time.
- CDN-Side Composition: The app will be composed on the node closest to the user who requested it by using an edge-side markup language. This results in greater scalability and shorter wait time. Routing is handled at this level via urls to go from page to page.
- Origin-Side: The app is composed using server-side rendering, with routing always handled server-side.
In the lifecycle of an MFE, there comes a point where communication is necessary. For example, an MFE may need to communicate that a user is logged in or have other MFEs perform actions. The most common communication techniques are:
- Data Sharing via Local Storage (such as a JWT token)
- Parameter Passing via URL Query String
- Parameter Passing via Events (subscribe, emit and replay mechanism)
It is important to choose the appropriate communication technique based on the specific needs and requirements of your application.
What is a Module Federation?
Module Federation is a powerful plugin introduced by Webpack in version 5.0, released in 2020. It enables the separate development of remote modules, allowing for the creation of a single application with ease. This plugin facilitates the implementation of an architecture based on the Microfrontends approach.
Let's imagine that our target business is an e-commerce site. After careful analysis, we identify three entities that can be designated as MFEs: the product list, the shopping cart, and potentially other components. The responsibility of implementing these MFEs is assigned to separate teams. Once the development of the MFEs is completed, the teams configure Webpack to make the remote modules available.
Basically, for exported modules the Module Federation plugin exposes three properties:
- Name which describes the name of the module.
- Filename by default, it is named remoteEntry.js. This file acts as a manifest and provides all the necessary directions to import the remote module, including the name of the module, aliases, dependencies, and references to bundles. It is generated when the remote module is compiled, and those who use it can import it using its URL.
- Exposes or a property that allows the paths to be made explicit via path to reach the functionality exposed by the module.
Let's now see how the team in charge of the app-shell will import the modules to merge them into one app. Firstly, we need to clarify what an app-shell is. Basically, it can be defined as a container of remote modules where part of the implementation of the MFE basics takes place.
In client-side composition, the app-shell has the responsibility of not only composing the app but also providing the tools that allow the MFE to communicate and manage routing. With the proper implementation of the modules, but especially of the app-shell, we can have an architecture that is closest to the theoretical ideal of MFEs, which is low coupling, separation, and decentralization of modules managed by separate teams
The configuration of the Module Federation plugin for our app-shell has some differences. Since the app-shell's function is to import, rather than export, we use the "remotes" property for this purpose. This property defines where we can find the remoteEntry.js file that remote modules expose. The structure is straightforward: we use the name of the remote module as the key (e.g., "products" or "cart") and then indicate the module name with the "@" symbol followed by the URL. This way, our app-shell bundle will contain all the necessary information to merge our modules and create our app.
In conclusion, the Module Federation plugin in Webpack 5 has revolutionized the software architecture by allowing dynamic code loading and sharing of dependencies between different applications. By leveraging this plugin and properly configuring the app-shell, we can achieve an architecture that closely aligns with the theoretical ideal of MFEs, with low coupling, separation, and decentralization of modules managed by separate teams.
It's important to note that this approach is relatively new. The community is actively involved and new frameworks are being created to simplify the implementation process. Other bundlers, such as Vite, are also providing plugins to developers with some differences compared to Webpack.
It is crucial to reiterate that this approach should only be taken if it aligns with the domain's requirements. If the potential problems outweigh the benefits, it may not be the right choice. Currently, big tech companies like Dazn, Facebook, and Zalando are utilizing this approach, keeping in mind the basic principles of MFEs and applying unique solutions to ensure optimal user experience and high scalability.
Author: Marco Bartiromo, Frontend Developer @Bitrock