Articles

Module Federation Explained: Sharing Code at Runtime

Module Federation lets separately built and deployed JavaScript apps load each other's modules at runtime. How hosts, remotes, and shared dependencies work.

Takina Takina · · 5 min read
Code editor showing a component's source

Module Federation is a bundler feature that lets one JavaScript application load modules from another, separately built and separately deployed application at runtime. Instead of publishing shared code to a package registry and rebuilding every consumer when it changes, a team exposes modules from its own deployment, and other apps fetch them in the browser when needed. It first shipped as a core feature of webpack 5 and has since been adopted by other bundlers through native support or plugins.

It is the most common technical foundation for micro-frontends, where several teams own different parts of one user-facing product.

The core vocabulary

Module Federation has three concepts worth getting straight before looking at any configuration.

  • Remote — an application that exposes modules for others to consume. A checkout team might expose a CartWidget component.
  • Host — an application that consumes modules from one or more remotes. The main shell of a storefront might load the cart widget from the checkout team.
  • Shared — dependencies that hosts and remotes agree to load only once, such as React or a design-system library, so the page does not ship two copies.

An application can be both a host and a remote. The roles describe a relationship, not a type of app.

How it works at runtime

When a remote is built, the bundler emits a small manifest script, conventionally named remoteEntry.js. It is not the remote’s code; it is a lookup table that says which modules are exposed, where their chunks live, and which shared dependencies they need at which versions.

When the host needs a remote module, roughly this happens:

  1. The host loads the remote’s entry script from the remote’s URL.
  2. The host and remote negotiate shared dependencies. If the host already loaded a compatible version of React, the remote uses it instead of downloading its own.
  3. The host requests the exposed module. The remote’s container fetches the relevant chunks and returns the module.
  4. The host renders or calls it like any other module loaded through a dynamic import.

Because the remote is fetched at runtime, the checkout team can deploy a new version of the widget and every host picks it up on the next page load, without a host rebuild.

A minimal configuration

Here is the shape of a webpack configuration for the remote:

// checkout/webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
const deps = require("./package.json").dependencies;

module.exports = {
  // ...entry, output, loaders
  plugins: [
    new ModuleFederationPlugin({
      name: "checkout",
      filename: "remoteEntry.js",
      exposes: {
        "./CartWidget": "./src/CartWidget",
      },
      shared: {
        react: { singleton: true, requiredVersion: deps.react },
        "react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
      },
    }),
  ],
};

And the host:

// shell/webpack.config.js
new ModuleFederationPlugin({
  name: "shell",
  remotes: {
    checkout: "checkout@https://checkout.example.com/remoteEntry.js",
  },
  shared: {
    react: { singleton: true, requiredVersion: deps.react },
    "react-dom": { singleton: true, requiredVersion: deps["react-dom"] },
  },
});

The host then imports the remote module by its federated name:

const CartWidget = React.lazy(() => import("checkout/CartWidget"));

Wrapping it in React.lazy and a Suspense boundary keeps the remote off the critical path and gives you a place to show a fallback while it loads.

The bootstrap pattern

A common first error is “Shared module is not available for eager consumption.” It happens because the host’s entry file imports React synchronously before the federation runtime has had a chance to negotiate which copy of React to use.

The standard fix is an asynchronous boundary. Move the real application startup into a separate file and load it dynamically:

// index.js
import("./bootstrap");
// bootstrap.js
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);

That one dynamic import gives the runtime a moment to initialize shared scopes before anything consumes them. The eager: true option on a shared dependency is the alternative, but it bundles that dependency into the initial chunk, which undoes part of the point.

Shared dependency options

The shared block is where most of the real design decisions live:

  • singleton — only one copy may exist on the page. Required for libraries that keep global state or rely on identity, such as React and most state-management libraries.
  • requiredVersion — the semver range this app needs. If the loaded copy does not satisfy it, the runtime warns, or falls back to loading the app’s own copy when singleton is not set.
  • strictVersion — turn a version mismatch into a hard error instead of a warning.
  • eager — include the dependency in the initial bundle rather than loading it asynchronously.

Getting these wrong is the most common source of subtle bugs, such as two React copies on one page breaking hooks.

Module Federation vs other ways to share code

npm packagesImport mapsModule Federation
When code is resolvedBuild timeRuntime, by the browserRuntime, by the bundler’s runtime
Deploy independentlyNo, consumers rebuildYesYes
Shared dependency negotiationPackage manager dedupes at installManual, one URL per specifierBuilt in, with version ranges
Tooling requirementAny bundlerNative browser featureBundler with federation support
Best forStable librariesUnbundled or lightly bundled appsMulti-team apps with bundled builds

Import maps solve a related problem natively in the browser, mapping bare specifiers to URLs. Module Federation adds version negotiation and chunk management on top of a bundled build, which is why the two are sometimes combined.

Trade-offs to plan for

Runtime composition moves failure from build time to the browser. If a remote is down or ships a breaking change, the host finds out in production. Treat exposed modules as public APIs: version them, test the contract, and wrap remote components in error boundaries.

Other recurring costs:

  • Types. TypeScript cannot see a module that only exists at runtime. Teams generate and publish type declarations for exposed modules or use tooling that syncs them.
  • Loading waterfalls. A host that loads a remote entry, then a chunk, then that chunk’s own dependencies adds round trips. Preloading entry scripts helps, as does keeping exposed modules coarse.
  • Styling collisions. Separately built apps can ship conflicting global CSS. Scoped styles or a shared design system avoid surprises.
  • Organizational fit. If one team owns everything in a single repository, regular code splitting gives you most of the benefits with none of the coordination overhead. The monorepo vs polyrepo choice often decides whether federation is worth it.

The takeaway

Module Federation lets independently deployed apps share modules at runtime: remotes expose code through a small entry manifest, hosts load it on demand, and both negotiate shared dependencies so the page loads one copy of each. It is a strong fit for multi-team frontends with separate release cycles. Use singletons for stateful libraries, add an async bootstrap boundary, treat exposed modules as versioned APIs, and reach for plain code splitting when one team owns the whole app.