Quizzr Logo

Micro-Frontends

How to Choose the Right Micro-Frontend Composition Pattern

Compare runtime integration methods like Module Federation and Single-spa to determine the best orchestration strategy for your application.

ArchitectureAdvanced15 min read

The Evolution of Frontend Orchestration

In the early days of micro-frontends, many teams attempted to share code via build-time integration. This involved publishing components as private NPM packages and installing them into a shell application. However, this approach quickly revealed a significant bottleneck known as the dependency hell of coordinated releases.

When a shared component required a critical bug fix, every consuming application had to be updated, recompiled, and redeployed. This tightly coupled release cycle defeated the primary goal of micro-frontends: independent deployment. Teams found themselves waiting for other squads to finish their pipelines before they could ship their own features.

Runtime integration emerged as a solution to this problem by shifting the composition of the application from the build phase to the browser. Instead of bundling everything into a single artifact, the browser fetches the required modules on demand from different servers. This allows a team to update a single micro-frontend without forcing the rest of the system to rebuild.

Choosing between runtime strategies requires a deep understanding of how you want your applications to interact. The two most prominent methods today are Single-spa and Webpack Module Federation. Each offers a distinct philosophy on how to handle the lifecycle, routing, and shared dependencies of a distributed frontend system.

The fundamental goal of micro-frontends is not just code splitting, but the decoupling of deployment pipelines so that teams can move at their own velocity.

Defining the Runtime Orchestrator

A runtime orchestrator acts as the glue that binds disparate applications into a single user experience. Its main responsibilities include managing the browser URL, loading the correct bundles for the current route, and handling the mounting and unmounting of those bundles into the DOM.

Without a robust orchestrator, you risk conflicts where two micro-frontends try to control the same global variables or CSS classes. A well-designed orchestration layer provides isolation while still allowing for a cohesive navigation experience that feels like a single-page application to the end user.

Single-spa: The Framework-Agnostic Lifecycle Manager

Single-spa is one of the oldest and most mature frameworks for building micro-frontends. It treats each micro-frontend as an application that exports specific lifecycle functions: bootstrap, mount, and unmount. This standardization allows you to mix and match different frameworks, such as React, Vue, and Angular, within the same shell.

The core of a Single-spa setup is the root config, which is a simple JavaScript file that defines when each application should be active. It uses an activity function to check the current URL and decide which remote bundles to download and execute. This centralized routing control makes it easy to visualize the entire application structure in one place.

One significant challenge with Single-spa is that it requires a specialized loading mechanism, often involving SystemJS. SystemJS provides a way to map module names to URLs at runtime using an import map. While this setup is powerful, it adds a layer of configuration complexity that can be intimidating for teams used to standard bundling workflows.

javascriptSingle-spa Root Configuration
1// The root config orchestrates when each app is loaded
2import { registerApplication, start } from 'single-spa';
3
4registerApplication({
5  name: 'inventory-dashboard',
6  // Load the bundle via a dynamic script or SystemJS
7  app: () => System.import('inventory-mfe'),
8  // Only mount this app when the URL starts with /inventory
9  activeWhen: (location) => location.pathname.startsWith('/inventory'),
10  customProps: {
11    authToken: 'abc-123-xyz'
12  }
13});
14
15// Initialize the orchestrator
16start();

By using this model, the inventory team can deploy a new version of their bundle to a content delivery network and update their entry in the import map. The main application will automatically pick up the new version on the next page refresh without needing a new build of the shell or other micro-frontends.

Managing Shared State in Single-spa

Sharing state between Single-spa applications is typically handled through custom events or a shared observable store. Since these applications are physically separate bundles, you cannot easily import a shared singleton instance of a Redux or Pinia store at the build level.

Most teams implement a cross-micro-frontend communication layer using the browser native CustomEvent API. This allows applications to remain decoupled; one app can dispatch an event like user-logged-in, and any other interested app can listen for it without knowing the internal implementation of the dispatcher.

Module Federation: The Distributed Mesh

Introduced in Webpack 5, Module Federation represents a paradigm shift in how we think about sharing code. Unlike Single-spa, which relies on a central orchestrator to manage lifecycles, Module Federation allows applications to dynamically consume code from one another. Any application can be a host, a remote, or both simultaneously.

The primary advantage of Module Federation is its native integration with the build tool. You define which modules you want to expose and which remotes you want to consume directly in your Webpack configuration. This removes the need for external loaders like SystemJS and makes the developer experience feel almost identical to standard local imports.

Module Federation is particularly adept at solving the shared dependency problem. If multiple micro-frontends use the same version of React, the orchestrator can detect this at runtime and download the library only once. This optimization significantly reduces the total payload size delivered to the user, improving performance across the board.

javascriptWebpack Module Federation Setup
1// webpack.config.js in the 'checkout' micro-frontend
2const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
3
4module.exports = {
5  plugins: [
6    new ModuleFederationPlugin({
7      name: 'checkout',
8      filename: 'remoteEntry.js',
9      // Expose internal components for others to use
10      exposes: {
11        './PaymentForm': './src/components/PaymentForm.js',
12      },
13      // Define which remotes this app will consume
14      remotes: {
15        auth: 'auth@https://auth-service.example.com/remoteEntry.js',
16      },
17      // Prevent loading multiple copies of core libraries
18      shared: { 
19        react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
20        'react-dom': { singleton: true, eager: true }
21      }
22    }),
23  ],
24};

In this example, the checkout application exposes a payment form for the main container to use, while simultaneously consuming an authentication module from another service. The shared object ensures that only one instance of React is active in the entire browser tab, preventing the bugs that often occur when multiple versions of a framework fight for DOM control.

The Remote Entry Mechanism

Every Module Federation build generates a small manifest file called remoteEntry.js. This file contains a mapping of all exposed modules and their dependencies. When the host application starts, it first fetches this tiny manifest to understand how to load the actual logic containers efficiently.

This mechanism allows for extreme flexibility in deployment. You can update the payment form logic, rebuild the checkout micro-frontend, and the remote entry will point to the new chunks. The host application will resolve these new chunks at runtime without ever needing to change its own code.

Critical Comparison: Which One Should You Choose?

Choosing between Single-spa and Module Federation depends heavily on your team's existing infrastructure and goals. Single-spa is the better choice for organizations running a heterogeneous mix of frameworks. If you are migrating a legacy Angular application while building new features in React, Single-spa provides the necessary lifecycle wrappers to make them coexist.

Module Federation is the superior choice for teams that are already invested in the Webpack ecosystem and are primarily using a single framework. Its ability to handle shared dependencies with minimal configuration is a massive productivity boost. It feels more like a natural extension of the JavaScript module system rather than a separate orchestration framework.

Performance is another key differentiator. Single-spa relies on manual optimization and import maps to avoid duplicate loading of libraries. Module Federation automates much of this through its shared configuration, which can automatically negotiate versions and deduplicate packages during the initial handshake.

  • Framework Support: Single-spa excels at multi-framework environments; Module Federation is best for unified stacks.
  • Configuration Overhead: Module Federation is integrated into the build tool; Single-spa requires external loaders and root configs.
  • Dependency Management: Module Federation provides automatic deduplication; Single-spa requires manual management via import maps.
  • Maturity: Single-spa has a longer track record; Module Federation is the modern standard for Webpack-based builds.

It is also important to consider the blast radius of failures. In a Single-spa setup, a failure in the root config can bring down the entire application. Module Federation is more decentralized, but a breaking change in a shared dependency can still cause runtime errors if not managed with strict versioning rules.

Hybrid Strategies

It is possible, though complex, to use both strategies together. Some teams use Single-spa to manage the high-level routing and lifecycle of large sub-applications, while using Module Federation within those sub-applications to share granular components and utilities.

This hybrid approach is usually reserved for massive enterprises with hundreds of engineers. For most projects, picking one strategy and sticking to it will reduce the cognitive load on developers and simplify the CI/CD pipelines.

Implementation Pitfalls and Best Practices

The biggest pitfall in micro-frontend development is treating the browser like a server. Developers often forget that all micro-frontends share the same global environment, including the window object, local storage, and cookies. Without strict naming conventions, one application can easily overwrite the data of another.

Another common issue is CSS scoping. If two teams use the same generic class names like .btn or .container, their styles will collide. To prevent this, you should use CSS Modules, Styled Components, or unique naming prefixes for every micro-frontend. This ensures that the visual integrity of one module does not break when another is loaded.

Testing also becomes more complicated in a runtime-integrated world. Since the final application only exists in the browser at runtime, unit tests for individual micro-frontends cannot catch integration bugs. You must invest in end-to-end testing suites that run against a fully assembled environment to ensure that the orchestration layer is working correctly.

Finally, always implement a fallback strategy. If a remote server is down, your orchestrator should gracefully handle the error. Instead of showing a blank screen or a spinning loader forever, the orchestrator should display a user-friendly error message or a fallback component that allows the user to continue using other parts of the application.

Version Management and Contract Testing

To maintain stability, teams should adopt contract testing. This involves defining the expected API of a micro-frontend—such as the props it accepts or the events it emits—and running tests to ensure that updates do not break these contracts.

Using semantic versioning for your remote entries can also help. While micro-frontends aim for continuous deployment, tagging major versions allows you to roll back quickly if a runtime incompatibility is discovered in production.

We use cookies

Necessary cookies keep the site working. Analytics and ads help us improve and fund Quizzr. You can manage your preferences.