Microfrontends: module federation for runtime integration

Anıl Erciyes
8 min readJul 14, 2022

One of the biggest misconceptions about software engineering is that everything starts and ends with code. In a dimly lit room, we wear black hoodies and press the keys on our radiant green keyboard as the cool typing sounds get louder and louder every moment.

In fact, this is only a minor portion of the total picture and may even be seen as the final step. Because in order to reach that moment -the final stage of writing in practice-, the main challenge is to first conceptually decide how and where to write that code. We wish to proceed by carving out a convenient structure that will allow our product to be highly adaptable and scalable, and accomplishing such goal requires a meticulous architectural choice.

Therefore, before coding anything, we first attempt to create a theorotical roadmap by taking into account some factors: including the project’s requirements, the number of engineers that will work on it, and the technologies that will be used.

In the light of this, i’ll concentrate on the idea of Microfrontends today, which was emerged at the end of 2016. In fact, microfrontends can be considered as frontend’s interpretation of the backend transformation from monolithic to microservice architectures. As a consequence, it is possible to assert that one of the most trendy advancements in software architecture in recent years is no longer restricted to the backend, but also enlightens frontend engineers’ way of thinking.

Understanding the classical approach

Let’s start with monolithic frontend applications, which are familiar to us and may be the only ones we have seen so far. I’ll use React as an example since that is my strongest suit, but the concept applies to anything. In this scenario, you set up all your screens, pages, routes, hooks inside a single SPA. You write components, then put them together to create larger pages, you set routes to determine which pages the users will see when they go to which URL; even if it reaches tens of thousands of lines, all your services reaching the backend; all your UI from the smallest button to the most magnificent page, all your codes from specific to general, all your hooks… are under the same codebase. In addition, every frontend developer in your company work on this single, gigantic repo; any change is pushed to the main base where everything is held together.

Let me make a diagram to illustrate what’s going on here. For now, in our application, we only have two screens: one for displaying the products (and adding them to the cart) and one for final cart checkout page.

In this case, the diagramme of our application would probably be like :

Typical monolithic communication

This situation is the common practice for us. Under the same roof, our components typically communicate with one another using a state management/prop structure: in the end everything comes together to form a single, -possibly- enormous application.

So what’s the problem here?

1. Even after a very minor fix, the deployment cycle of a whole codebase starts from the beginning, which means lower realtime responsiveness to urgent situations

2. Tightly coupled, less flexible structure: When everything is connected in some way, the app is definitely more prone to encounter a situation where solving a problem in some part might break another part

3. Codebase grows TOO MUCH over the time: the new software developers joining us must make a significant effort to adapt to the project

4. For developers, working on a too large codebase results in not being able to find enough time to focus on the business side of the product they develop

5. It may be necessary to take decisions which affect the whole system, just in order to overcome the problem experienced only in a particular part of it

Introducing the solution

Here comes the microfrotends architecture as a way to overcome all these problems in the most flexible and scalable way possible. Microfrontends seeks to break the application down into its major roles and developing each one as a separate web application. It puts emphasis on minimizing the interdependencies between these numerous major features. Here, communicating these independent structures via an API is always welcomed. Heading back to the diagram:

What MFE looks like

Therefore, it becomes completely logical to divide the teams. One team only works on the ProductsPage project, while the other solely focuses on CartCheckout. Since their projects are not directly tied, they are free to make whatever decision they want about that particular setup. So one can use Angular while the other can use React; A package in one may not be in the other: they are free to choose whatever is the best way to do, independently.

In fact, we can also consider a third application, by adding a third team here and hold them responsible only for creating the “Container” application. That is, their task is to assemble the ProductsPage and CartCheckout sub-applications into one main container, in order to provide the users an impression of a single application. Even though different endpoints refer to different apps behind the curtains, container team can definitely create that singular feeling by using proper merging techniques.

Benefits of MFE approach

• Faster developer specialization in smaller codebases working on a specific feature

• Much more dynamic and reactive deployment process, as each application will have its own cycle

• To be able to use different packages according to different needs in different parts of the project, to choose the desired tech stack; more freedom to find a more specific solution to every problem

• Easier problem solving, not allowing a tightly coupled and chaotic code

Seems nice, but how to connect?

As you see, it is a bright idea to break up our main app into small standalone apps and enjoy these benefits, but the first question that comes to mind is how to combine it all. Secondly, although we manage to assemble all somehow, we have to make sure that all of them can work together consistently without problems. The reason is, for instance, if two applications try to use different versions of the same package, React (or any other package that can’t function under such circumstances) will crash and the application will stop working.

We have various techniques to accomplish the assembling part (which we refer to as “integration”) and smoothly get past these problems. Let’s explore the following two:

I. Build Time Integration

The first option that comes to mind, and clearly the most straightforward. Let’s see an example: ProductsPage development team completes their task and saves the source code to NPM. Now other teams can use NPM to download the package and include it in the project when they require this feature.

The biggest drawback is that this process restarts even after the smallest bugfix.

Isn’t it possible to automatically reflect the change in side A to side B?

II. Run Time Integration

Now we are slowly moving things forward. It will be possible to automatically pull the most recent version during runtime rather than having to use NPM on each occassion. In this article, I’ll cover Module Federation strategy that comes with Webpack 5.

For demonstration purposes, I use the create-mf-app (create module federation app) package, which gives me a quick setup. Below I will show you how my “Home” app running on localhost:8080, accesses a component in real time from my “Products” app running on localhost:8081. In this method, changes in Products are instantly reflected in anywhere it is used.

First, let’s create the home app on 8080.

“Home” app is active on 8080

Now, let’s create the products app on 8081.

“Products” app is active on 8081

I am writing a component like below in the Products app:

We are in the Products project (8081)

Then, in the webpack configuration, in the exposes:{} part, I specify what this app (Products) will serve to other apps:

Arranging what :8081 will serve to others

Now, I enter the webpack configuration of the home application. Here, from the remotes: {} part, I’m telling it to connect to the 8081 localhost where Products is on live and get what is served from there.

Arranging the Home (:8080) to take the served data from 8081

Now I can use Products in my code inside Home:

Note that from “products/Products” matches the name in Webpack config

Output:

Actually we are in :8080, Home! But we managed to take the served component from :8081.

It would be fruitful to talk about the terminology here. In the config, we see a field called “shared””. This is there to prevent the browser from loading the same package multiple times. Shared modules help us, so that we don’t initiate the identical load from scratch for every instance.

Anyways, in some packages, for example “React” itself, running two different versions at the same time results in an error. “Singleton Loading” is an additional setting to ensure that only one instance of this package exists. You can see how it is used above.

Of course, the hidden tricks of Microfrontends architecture may be written for dozens of pages: what to do with states, correct logic to share JWT authentication, the cleanest path to follow in chaotic package conflicts, obtaining the most optimized bundle… Even though it takes a solid effort to advance, I believe, it is definitely worth. I tried to outline the main idea behind the microfrontends architecture and showed you with a simple example how two different React applications can be connected to each other in realtime with Module Federation technology.

To wrap up, I believe it is safe to state that microfrontends can be preferred in large projects on which many people work, in order to write code as efficiently as possible with fewer dependencies, a quick deployment cycle, and specifically tailored solutions.

--

--

Anıl Erciyes

Researcher Computer Engineer at Vehicular Networks & Intelligent Transportation Research Laboratory, AWS Certified