Micro-Frontends: State of the Craft

Can we apply modular, scalable microservice patterns to the front end? Sure, why not?

Brian Sokol
Slalom Build
12 min readJun 18, 2024

--

Here’s a scenario: your organization likes to compartmentalize. There are departments for products, billing, sales, marketing, communications, etc. Years ago, you adopted a microservice architecture to reflect your org structure. Each department now has its own dev teams and maintains its own back-end systems. But your web app is a monolith, a single massive application that talks to all these microservices. Can’t we apply the same modular, scalable patterns to the front end? Sure, why not? Let’s talk about micro-frontends.

What Are Micro-Frontends?

First, let’s address the spelling, which doesn’t have a consensus as of this writing. “Micro-frontend” (as opposed to “microfrontend” or “micro frontend”) is my preferred spelling, and it’s the one I’m going to use when I’m not avoiding the matter entirely by using the abbreviation “MFE.”

Micro-frontends are small web apps that are composed together to create a seamless experience for the user. Each MFE is separately maintained, built, tested, and deployed. They can be owned by different teams. Like microservices, a micro-frontend-based web app can be polyglot, although there are some challenges with this approach, as I’ll explore later. This decomposition is sometimes referred to as “horizontal slicing.”

Typically, each page in a micro-frontend-based web app contains multiple MFEs on-screen at the same time. MFEs are orchestrated using an app shell, which is essentially the parent web page or web app responsible for deciding which MFEs appear at any given route. The app shell also determines the layout.

Diagram of a web page showing which parts could be broken into their own microfrontends, including product detail, add to cart, consumer reviews, financing, and related product suggestions. It also shows sharing common data through utility microfrontends that handle the currently selected product information and user information.
Example product detail page decomposition

Unlike microservices, which can be entirely independent, MFEs will endure some coupling. They will run in the same runtime environment (the user’s browser), they may share dependencies, and they likely should be sharing the same visual language (UX).

Patterns That Are Sometimes Called Micro-Frontends That Are beyond the Scope of This Article

One common solution for dividing and conquering a web app is to break up the site vertically by feature. Each high-level route becomes its own single-page app, for example. The term “micro-frontend” has grown to encompass this kind of pattern by definition, though it entails a different set of considerations than the horizontally sliced MFE apps mentioned earlier. This pattern is well established and will be beyond the scope of this article, except as a point of contrast with horizontal slicing.

When Should You Use Micro-Frontends?

Micro-frontends come with their own set of challenges, so you want to make sure the juice is worth the squeeze. As I described in the opening scenario, organizations with compartmentalized dev teams will benefit most from this pattern. The ability to build and deploy each MFE entirely independently may be worth the overhead of their orchestration.

If you have a single dev team, or your dev teams cut across domains, you may find MFEs to be more trouble than they’re worth.

Your organization may be served well by vertical slicing, where your site perhaps has informational pages maintained by marketing, a cart/checkout page owned by the payments team, etc. Horizontal slicing allows for more dynamic pages. Imagine a product page that primarily contains a product listing from the sales team with the option to add it to your cart. The product listing could be composed of one or more MFEs, such as an image carousel, description, customer reviews, etc.

This page could also contain a cross-promotion MFE from the marketing team, a financing deal MFE from the finance team, and a cart widget from the payments team that shows your current cart and lets you edit it without leaving the product page. The “Add to Cart” button itself could also be an MFE owned by the payments team. New sections of the page can be added by updating the app shell. No need to coordinate with any of the other MFE owners.

Dev team organization isn’t the only factor, however. As I mentioned, MFE-based apps can be polyglot, meaning they don’t need to share the same parent framework. A single page can contain a section running in React, while another runs in Svelte, and yet another is just good old-fashioned HTML and CSS. This can be useful if you are trying to migrate your website from one framework to another over time. However, this approach leads to additional overhead for the user (more JavaScript downloaded, more running in memory at once, etc.). Keep this in mind when deciding to explore this route.

Common Implementations

Micro-frontends don’t have an agreed upon implementation, and several methods are currently being explored.

Iframes

The oldest method of composing a web page is the use of inline frames, or iframes. These are essentially web pages embedded inside other web pages loaded at runtime, which superficially meets the definition of a micro-frontend.

Iframes have several key downsides, however. Responsive designs are notoriously challenging with iframes. Communication between parent and child iframes, or between the content within child iframes, is also problematic.

Module Federation

In order to reduce that extra overhead on the user, module federation is used to share dependencies between MFEs. Each MFE maintains a list of dependent libraries that it will attempt to load. However, if another MFE has already loaded an acceptable version of the same dependency, it will simply be shared instead of being fetched again.

Since dependencies are loaded on a first-come, first-served basis, your MFE may get a different version of a dependency each time the page loads, depending on which other MFEs have been loaded first. Although the dependencies are guaranteed to be within a specified version range, this does create variability. If a dependency outside of the specified version range is already fetched, another copy that is within the range will also be loaded. This can lead to a challenging balance when determining how restrictive to make your dependency ranges.

It’s an emerging pattern pioneered by Zack Jackson and implemented in webpack. It can be used with other bundlers through plugins.

Import Maps

One of the most popular MFE frameworks is Single-Spa, which by default uses import maps to manage dependencies. Like module federation, import maps help coordinate dependencies between MFEs. Instead of each MFE declaring its own dependencies, the app shell maintains a list that maps dependency requests to their concrete implementation. If the dependency is in the import map, it will be provided by the app shell. Otherwise, it’s up to the MFE to fetch its own dependencies.

Single-Spa also supports module federation.

Trade-offs

Import maps require every shared dependency to be in the map. MFEs are coupled to the version of the dependency that is in the map, and updates to those dependencies need to be coordinated if there are breaking changes. However, this approach ensures that only one version of each dependency will ever be loaded at one time, and you know the version in the import map is the version that will be loaded.

In contrast, module federation shifts the burden to the MFEs. All that is required to share a dependency is for two or more MFEs to declare their reliance on it. Dependencies are therefore loaded on a first-come, first-serve basis. For example, consider two MFEs that both rely on version 18.x of React. If the first MFE to load requests React 18.0.0, that version will be served to other MFEs that also request React 18. This could be an issue if your MFE depends on a bug fix in React 18.2.0.

It’s possible to override this behavior, but then you have two versions of React running in your app at the same time. This will work, but it isn’t ideal.

Communication Between MFEs

Even though you want your micro-frontends to be as decoupled as possible, there are many common situations where you want other MFEs to be aware of a state change in your MFE. MFEs that aren’t using iframes have a few options for communicating with each other.

Sharing a single app state (as one might do with Redux, NgRx, or similar libraries) is not recommended. While technically possible, this sharing introduces a very tight coupling between MFEs. Any change to the app state structure would then require all consuming MFEs to update in response. Similarly, storing values in localStorage and subscribing to localStorage change events creates the same coupling issue.

In frameworks such as Single-Spa, MFEs can simply export functions that other MFEs can import at runtime. Your MFE can execute code in another MFE as needed.

MFEs can also use browser events to signal state changes. MFE maintainers would need to agree on a common set of events and subscribe to the ones they care about. The built-in event emitter is the simplest tool for this, but other pub/sub libraries such as RxJS would also work for more complicated messaging.

Though it may seem counterintuitive, MFEs don’t need to provide a UI. MFEs that exist to provide shared functionality to other MFEs are known as utility MFEs. Utility MFEs can encapsulate the previously described patterns by maintaining an opaque internal state and exporting functions for other MFEs to interact with the internal state, as well as allowing MFEs to subscribe to those state changes.

Additional Challenges

I’ve mentioned issues and challenges quite a bit, so let’s address them. I would like to stress that these are challenges, not immovable objects. Careful consideration should be given to each of these topics before deciding to adopt a micro-frontend architecture.

Why SSR Is Hard

Server-side rendering (SSR) involves rendering front-end components on the server and sending up the resulting HTML as well as an initial payload of state data to hydrate the page, allowing the client-side framework to add interactivity.

There are several challenges to SSR with micro-frontends, not because it is impossible, but because adding in the possibility of multiple front-end frameworks adds complexity that few have been willing to tackle. Some benefits of micro-frontends are negated once SSR is added to the mix.

There are (at least) two ways to approach this. In a normal micro-frontend-based app, the app shell is loaded first, which then fetches the MFEs needed for that route. In the simplest solution, MFEs could be rendered by their own servers and hydrated in the browser. But in this case, even if parts of the site use SSR, the page isn’t truly server-side rendered. The app shell will still execute client-side before fetching the rest of the MFEs.

The more complicated solution is to also render the app shell server-side. This approach would involve examining the route and reaching out to each MFE server-side for its initial HTML and payload, which is then all combined and returned to the browser. This method requires every MFE to run its own web server, as opposed to existing as a pre-built bundle in a CDN.

Response headers from each MFE’s SSR servers would need to be merged. Data needed for hydration would need to be combined on the server and then dispersed on the client. Complicated orchestration is needed to ensure that the browser doesn’t have to fetch the same data that was already fetched on the server. While this is all technically doable, it can lead to additional coupling and can be more difficult to maintain. Additionally, any shared global state in the browser may not be available server-side.

To top it off, the normal challenges of SSR development are still present. What viewport size should I prerender for? Should I be sending styles for light mode or dark mode?

Global Layout

Most micro-frontend app shells are organized by route, using a layout engine. The app shell decides which global dependencies to prefetch, which MFEs to load, and where on the page they should appear. This requires coordination between the teams and/or an authority to coordinate between MFE owners. In other words, someone has to own the layout and manage the inherent coupling between MFEs at this layer.

Common Styling

Whenever I speak with our customers about a potential micro-frontend solution, I am inevitably asked how the site will maintain a unified look and feel if every MFE is its own separate island. It doesn’t matter how well architected your individual MFEs are if the user experience ends up feeling disjointed. There are multiple possible solutions, but all require some degree of management and coordination.

The simplest coordination approach involves creating a single shared master stylesheet accessible globally within the app shell, with micro-frontends referencing it in their components. However, this method has drawbacks. Each MFE then maintains its own component library, potentially leading to redundant work across teams. Major updates to the global stylesheet require coordination among MFE owners because changes take effect immediately across all MFEs. Alternatively, MFEs could bundle this stylesheet for predictable updates, though this practice results in unnecessary duplicate data sent to the browser.

Another option is to create a shared component library, including prebuilt, generic components that encapsulate styling. As with the global styling option, the component library could be provided globally or bundled with each MFE, with the same pros and cons. If necessary, MFEs can choose to use the global component library or override it with an older version temporarily while an upgrade is taking place.

Component libraries come with their own governance requirements. I recommend either forming a team to own the development of the component library or designating an authority that can gatekeep contributions from other teams to ensure quality and consistency.

Consider one additional factor in your decision: if your micro-frontends don’t use the same front-end framework (Angular and Vue, for example), you will need to maintain multiple component libraries. A global stylesheet will work for any front-end framework but again may lead to each MFE owner creating their own component library.

Web components may be a potential compromise if your front-end framework(s) of choice support them. Basic functionality and styling can be included in a web component and shared between micro-frontends. MFEs may still encapsulate those into their own internal component library, but at least the base functionality will be shared and not reimplemented.

Testing

Testing a micro-frontend architecture demands a multifaceted strategy. Comprehensive testing across the entire application is necessary to validate compatibility between MFEs. Establishing a governance model becomes essential to oversee the creation and maintenance of end-to-end tests that span multiple MFEs.

Micro-frontends can independently manage their unit tests for components, often requiring the use of mocks to simulate interactions with other MFEs and shared data logic.

Integration tests for utility MFEs are important to ensure that data is properly accessible from each MFE. Any changes to utility MFEs could require adjustments in consuming MFEs.

Automated end-to-end testing in a browser environment across the entire app is crucial to ensure that the user experience is consistent. Some bugs may not become apparent until the entire app is loaded together.

While the micro-frontend architecture may have less impact on certain types of testing (such as visual regression, accessibility, performance, and security testing), these aspects remain crucial components of a comprehensive testing strategy and should not be overlooked.

Other Random Considerations

Data sharing among micro-frontends can be complicated. While micro-frontends are intended to be self-contained, there will inevitably be some common data, such as user information. If this sharing isn’t well-coordinated, different MFEs might redundantly fetch the same data, leading to unnecessary network usage.

Employing multiple front-end frameworks simultaneously will result in increased data and processing overhead. Although frameworks can be globally loaded and shared, you will still bear the burden of running them concurrently.

So, Should You Use Micro-Frontends?

Micro-frontends address specific challenges but also introduce additional complexity. I advise against adopting micro-frontends unless your organization has a clear need for them and a mature development workflow.

Micro-frontends introduce development challenges, including stricter dependency management, developing in isolation versus running in the shell app, and coupling between MFEs related to the visual language of the app. While these are indeed challenges, the benefits outweigh the downsides if your organization is struggling with a single large shared codebase.

Micro-frontends may be a good solution for your organization if you’re encountering issues coordinating multiple teams working on the same web application. If your user interface can be logically divided into smaller web apps with minimal communication between them, then the overhead of the micro-frontend pattern may be worth it to improve organizational development velocity.

As the pattern develops, that overhead will hopefully be reduced. Even if you feel your organization meets the above criteria, I recommend piloting the pattern with a smaller app to understand what will be required of your development teams before committing to this approach. It will be difficult to migrate away once you’re on this path.

Further Reading

--

--

Brian Sokol
Slalom Build

Director of Software Engineering @ Slalom Build Chicago. TypeScript, Node, React, and Angular. Contributor to https://medium.com/slalom-engineering.