Module Federation—Federated Application Architectures
There’s been a lot of excitement in the community about the upcoming module federation feature in Webpack 5. It will enable a much simpler and more practical setup for architecting and developing micro frontends compared to previous approaches.
Micro frontends come in many flavours. In the context of this article, it refers to frontend applications that can be developed and deployed independently and compose to make up a greater digital product. Federated applications are a superset of micro frontends that can enable them, but also other architectures that benefit from dynamically sharing code between independently deployed applications—some of which will be discussed in this article.
Module federation introduces a few new concepts that are worth explaining before going deeper.
Host: The first webpack build (application) that initialises when loading the page.
Remote: Webpack build that provides consumable remotely consumable modules.
Bi-directional host: A webpack build that is both a host consuming remotes and a remote being consumed by other hosts.
Vendor federation: Allows a part or all of a host or remote’s npm module dependencies to be declaratively shared at runtime, no matter where they are loaded from. This solves one of the big performance issues of micro frontends. Additionally, Beta 17 introduced more granular version control and the option to enforce only one instance of a given dependency at runtime.
- Hosts and Bi-directional Hosts specify the consumed remotes via
- Remotes and Bi-directional Hosts use
exposesto specifies one or more modules that will become available externally
sharedspecifies which npm modules are available to re-use or be re-used (vendor federation).
Mitigate breaking functionality in a Host
Let’s first address the elephant in the room: There is an inherent danger consuming an independently updated dependency. It requires solid testing practices, well-defined contacts, and all the teams working on remote modules to be mindful of breaking changes, and orchestrating changes with all consumers. Ideally, hosts and remotes should have a thin, well-defined contract, but this might not always make sense (e.g. for a component library).
Some strategies to reduce the risks can include:
- Integration tests across apps
- Include members of each host to PRs
- Mono-repo with cross-app typing
- Hosts and remotes have a thin, well-defined immutable contract
- Route based remote versioning (e.g. deploying each remote with a version or hash
my-host.com/VERSION/remoteEntry.js. This could be used for breaking changes (if not avoidable), environments or every release.
Using route-based versioning, it’s also possible to have separate evergreen or non-breaking LTS versions of a specific remote.
Federated Application Patterns
Let’s explore some of the patterns and use-cases for module federation.
Evergreen Design System/Component Library
An evergreen remote is one of the simplest types of federated application—a shared remote, such as a Design System or a Component library, gets independently released, which automatically updates it for all consumers. This could be useful to ensure all web-properties follow the latest corporate branding, without each app team having to invest time on updates.
This could be a good starting point for organizations to explore a federated application architecture and begin to define and implement the boundaries and processes needed to ensure safe, continuous updates.
Some use-cases that would be good candidates for separately deployed shared remotes:
- Design systems
- Component libraries
- Application shells
- Alternative distribution models for widgets used by internal or external consumers
- Shared toolkits
Multi-SPA Module Sharing
Reuse existing exported functionality, such as components across stand-alone single page applications. The benefits are:
- Simplifies the deployment process as modules don’t need to be released separately
- Domain knowledge stays within the team responsible for it
- Consumers get updated automatically
- The Product team does not wait for the checkout team to finish their work before they can release a new products version
- Shell provides (top-level) routing, lazy loading the remotes when needed
- Shell provides the framework and other shared dependencies that get reused by the lazy loaded remotes
- No page-reload when moving between remotes
- Vendor federation allows commonly used npm packages to be re-used when routing between remotes
Same as the Shell-driven federation above, but with multiple shells.
- Multiple brands
- Brand B does not need all remotes, or has separate implementations
With big power comes big responsibility, which is certainly true for federated applications. The ease of updating modules for any consumer on the fly has a lot of potential, but also makes it easy to unintentionally break functionality on host when teams and code-bases are more independent from each other and don’t have good governance models in place.
Another potential problem is type safety (which will be discussed with the TS team). There are already some partial solutions for a mono-repo setup (I have a PoC here and a proposed build based solution has been mentioned here) but not for remotes that live in other repos.
There could also be concerns using Federated Applications in security-sensitive environments (e.g. getting control over any one of the remotes could get attackers access to the runtime).
These issues aside, there it is a good fit when needing an “evergreen” module, sharing functionality with a thin rigid contract or when using a private or public npm registry is not an option (Deno might be an option for the server-side, too).
Want to read more? My colleague Andrew Smith has a superb high-level article on Federated Applications, if you want code examples—here are some examples of different configurations and an example in Angular.