DEV Community 👩‍💻👨‍💻

Cover image for How we used module federation to implement micro frontends

Posted on

How we used module federation to implement micro frontends

A while ago I posted a question to the community about problems I've encountered when implementing a micro frontend architecture and whether this is common or if there is a better way.

Thank you to those who responded.

Not so long ago I was a involved in a project as a tech lead leading one of many teams to build micro frontends using module federation. My role was to work with the lead architect and other teams in guide my own team through the implementation, finding solutions to problems along the way. It was arguably one of the best roles I've held so far. I'd like to share how we went about it and share our solutions to certain problems we faced.

We were building several applications that spanned not only across a large organization but also across it's partners and subsidiaries as well. It was to be a giant web of interconnected micro frontends so the solution had to scale very well in terms of being able to roll out new features and fixes quickly across all affected apps. Module federation was seen as a way to enable this.

Module federation is a feature of Webpack 5 which allows an application to consume code from another application at runtime using either static or dynamic imports.

You can read more about module federation here

Our Micro frontend (MFE) applications consisted of a top level shell application that consumed federated mini-apps which we referred to as MFE components. These MFE components can themselves consume other federated MFE components, creating a tree of nested MFE components. The shell application acts as the entry point for end users and handles things like authentication, logging, analytics, responsibilities that it could delegate to other MFE components.

To help illustrate an MFE built using module federation, picture a shopping app that consumes a catalog MFE component and a shopping-cart MFE component. The shopping-cart component also consumes a payment MFE component. The shell application does not know that the shopping cart component consumes a payment component, also the catalog component does not know about the shopping-cart component. All of these components hosted inside the shell application form a user journey from searching to ordering to paying.

The following image is an attempt to give a visual representation of how such an app could be stitched together. Each colored dotted box represents a MFE component embedded in the shell application.

example of a micro frontend application

Now imagine that these MFE components can be reused in many applications, for example the catalog MFE component could also be used in a back office application to list out the products currently being sold and could render edit and remove buttons for each product based on roles; more on that later. If you later decide to change the look and feel of the catalog component or fix a vulnerability, then both the public facing application and the back office application could be updated as soon as the new code is deployed and without needing to redeploy the shell applications. This is why module federation chosen, the ability to roll out changes to multiple applications at scale without having to rebuild and redeploy all of those applications. This all sounds good on paper but it requires discipline within the development teams to keep the architecture clean, as we know that even a seemingly minor changes can have unforeseen consequences. Only time will tell how well this strategy will actually work in practice.

There are many aspects to consider when building micro frontends. What follows are the areas that we probably spent the most time on trying to get right.

Communication between the shell and the MFE components is done via props and browser events. Props allow a consumer to configure the MFE component or pass in callback functions. Browser events are published by MFE components and used for global/cross cutting notifications which would be difficult to implement cleanly using callbacks. In the above example the catalog component would publish an ADD_ITEM event that the shopping cart listens for so that it knows the user wants to add an item to the cart.

We defined very specific contracts for each MFE component and events. Typescript was chosen as the way to enforce those contracts at development time. To share these contracts they were published in an npm package.

We followed semantic versioning but we had a rule that there can be only 1 major live version at a time; maybe 2 in rare cases. The reason for this rule was because only a major version is meant to contain breaking changes to a contract thereby forcing a consumer to pull the latest contracts, test and redeploy their app. In the case of minor and patch updates, as soon as a component is deployed into production the consuming app will immediately get the latest changes without needing to be redeployed.

Multiple domains and multiple environments
It is common for an application to be running in different environments, Dev, Staging, Prod but in a module federated world this presents a bit of a challenge. How do you tell a consumer of an MFE component where it should consume it from? These are the methods I've seen used in the past:

  • Bundling a json containing the urls of the consumed MFE component for each environment
  • Using the promise support in the module federation plugin to pull the configuration from an remote source at runtime.
  • Using module federation dynamic loading to again pull the configuration from a remote source at runtime
  • Baking the urls into the application at build time.

While these methods work, and I have used the promise based method for an earlier MFE, they do have their challenges. Particularly when different domains are involved as with our case, because you run into trouble with CORS and whitelisting the domains of all consumers of each MFE component isn't practical. We solved this by utilizing the path based routing capability of Akamai to route requests from the shell application to static assets or experience APIs. This means that the consumers don't need to know the location of the components they are consuming and since all requests looked like they came from the same domain, CORS also becomes a non-issue.

This diagram illustrates how this works.
Web request routing diagram

Analytics and Logging
The MFE components and the shell had a need to report analytics and logging but we wanted this to work at the shell level. Therefore browser events were raised by each MFE component and capture by the shell. The shell was then responsible for sending those messages to the analytic and logging platforms.

Npm package dependencies
Module federation allows federated components to bring their own npm dependencies and you can use the Medusa dashboard to see what dependencies are in use. Some packages such as React can only exist once in an application and Module Federation allows you to specify a package as being a singleton so that it will only download it once. Though React versions 16 through to 18 are mostly backwards compatible as long as everyone is using only the features that are supported by the shell React version. If the shell wants to upgrade the version of a singleton dependency that is not backwards compatible then all MFE components would need to upgrade first.

Each MFE component can introduce sub routes but the shell is not meant to know this as it is meant to consume MFE components without knowing how they work. As such, the thinking was that an MFE component would bring its own router, and this is something we had trouble with. We had standardized on React Router 6 and without doing hacks we couldn't keep the browser history across all of the routers in sync so we settled on having just one router in the shell. Another issue we had was federating a single MFE component that contains the entry point view and the routes. This couldn't be done because when the user navigates so that an MFE component is not being rendered anymore its routes will stop working since they are also not being rendered. Therefore we decided that an MFE component should also federate a separate routes component which is then nested into the shells router. Maybe other routers do this better but I would at least recommend standardizing on a specific router. If an MFE component consumes another MFE component that has routes, then since consumers are not meant to know about the second MFE component, I think the first MFE component could re-expose the second MFE components routes as its own routes, and so on.

If anyone has a better way to handle routing within a federated app then I would be interested to learn about it.

Role based access
One last thing was how we used role based access to control the visibility of UI elements within MFE components. Basically the shell applications consumed an authorization MFE component that was responsible for handling things like redirecting the user to the login page, refreshing the access token, caching basic user and role information, etc. Each MFE component also consumed a Role MFE component which had access to the cached role information through React context API. This works because the shell application + the MFE components it consumes become a single app.

The MFE components wrap UI elements that are controlled by role with the Role MFE component and pass in the required role(s) as a prop. The Role MFE component decides to either render or not render it's children based on whether the user had the required role(s). This isn't really related to module federation and micro frontends but I think it is a nice pattern.

This post has become quite long and I've talked about the main areas I wanted to cover so I think I'll leave it there folks. If you've made it this far then thank you for reading and I hope others embarking on micro frontends using module federation can take some food for thought from it.

Top comments (0)

🌚 Browsing with dark mode makes you a better developer by a factor of exactly 40.

It's a scientific fact.