Module Federation
The Problem: Sharing Code Without Rebuilding
You have three frontend teams, each deploying their own app independently. The header team ships on Mondays. The product team ships whenever they want. The checkout team has a two-week cycle. This is the micro-frontend dream — independent deployment.
But all three apps need React. All three use the shared design system. All three import a date formatting utility. Without coordination, each app ships its own copy of everything. Three copies of React. Three copies of the design system. Users download all of it.
Build-time solutions (npm packages, monorepo shared modules) force everyone to rebuild and redeploy when shared code changes. That kills the independent deployment benefit.
Module Federation solves this: applications share JavaScript modules at runtime, without rebuilding the consumers. Team A updates the design system. Teams B and C get the update on their next page load — no rebuild, no redeploy.
Think of Module Federation like a streaming service for JavaScript modules. Without it, every app downloads its own DVD copy of every shared library (bundled in). With federation, apps stream shared modules from a central source at runtime. When the source updates, everyone gets the new version on their next "play" (page load). No one needs to buy a new DVD.
Core Concepts: Host and Remote
Two roles define Module Federation:
Remote — an application that exposes modules for others to consume. It says: "Here are the components/utilities I'm making available."
Host — an application that consumes modules from remotes. It says: "I need a ProductCard from the catalog app."
The powerful part: an application can be both simultaneously. Your shell app might consume the header from the header remote while exposing authentication utilities to the checkout remote.
Remote Configuration
// Product Catalog app — webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'catalog',
filename: 'remoteEntry.js',
exposes: {
'./ProductCard': './src/components/ProductCard',
'./ProductGrid': './src/components/ProductGrid',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};
The filename: 'remoteEntry.js' creates a manifest file that hosts download to discover what the remote exposes.
Host Configuration
// Shell app — webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
},
}),
],
};
Using Remote Modules
// In the Shell app — import from the remote as if it were a local module
import React, { lazy, Suspense } from 'react';
const RemoteProductCard = lazy(() => import('catalog/ProductCard'));
function HomePage() {
return (
<Suspense fallback={<CardSkeleton />}>
<RemoteProductCard productId="abc123" />
</Suspense>
);
}
The import 'catalog/ProductCard' looks like a normal import, but at runtime it:
- Fetches
remoteEntry.jsfrom the catalog app's CDN - Discovers the ProductCard module's chunk URL
- Downloads and executes that chunk
- Returns the module to the consumer
Shared Dependencies and Version Negotiation
The shared dependency system is where Module Federation gets really clever. When multiple federated apps declare the same dependency as shared, the runtime negotiates which version to use.
// App A
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
lodash: { requiredVersion: '^4.17.0' },
}
// App B
shared: {
react: { singleton: true, requiredVersion: '^19.0.0' },
lodash: { requiredVersion: '^4.17.20' },
}
Singleton Mode
For libraries that must have exactly one instance (React, Redux), singleton: true ensures only one copy loads. If App A loads React 19.0.0 first, App B reuses that same instance instead of loading its own.
Version Negotiation
Runtime negotiation:
App A needs lodash ^4.17.0, has lodash 4.17.15
App B needs lodash ^4.17.20, has lodash 4.17.21
→ Both requirements satisfied by 4.17.21 (highest compatible)
→ Only lodash 4.17.21 is loaded
→ Both apps use the same instance
If versions are incompatible (one needs lodash 3.x, another needs 4.x), each app loads its own version. The federation runtime handles this transparently.
The eager and strictVersion Options
shared: {
react: {
singleton: true,
strictVersion: true,
requiredVersion: '^19.0.0',
eager: false,
},
}
eager: true— bundles the shared module into the entry chunk instead of loading it async. Avoids an extra network request but prevents sharing (each app has its own copy). Rarely what you want.strictVersion: true— throws an error if the loaded version doesn't matchrequiredVersion. Without it, federation warns but proceeds with the available version.
Module Federation 2.0
Module Federation 2.0 (also called Federation Runtime) evolved beyond the webpack-specific plugin. Key improvements:
Framework-Agnostic Runtime
Module Federation 2.0 provides a standalone JavaScript runtime that works with any bundler — not just webpack. You can use it with Rspack, Vite (via a plugin), or even without a bundler.
import { init, loadRemote } from '@module-federation/enhanced/runtime';
init({
name: 'host',
remotes: [
{
name: 'catalog',
entry: 'https://catalog.example.com/remoteEntry.js',
},
],
});
const ProductCard = await loadRemote('catalog/ProductCard');
Type Safety
One of the biggest pain points with Module Federation 1.0 was the lack of type information for remote modules. Version 2.0 introduces type hint generation:
# The remote generates type declarations
npx @module-federation/enhanced generate-types
The host can download these types for editor autocompletion and type checking on federated imports.
Manifest Protocol
Instead of a simple remoteEntry.js, Federation 2.0 uses a JSON manifest that describes available modules, their types, and version information. This enables smarter loading strategies and better developer tooling.
When NOT to Use Module Federation
Module Federation is powerful, but it's not the right solution for every team. Here's when to avoid it:
Don't Use It For Small Teams
If your frontend is maintained by 1-3 teams, the operational complexity of federation likely exceeds the benefits. A monorepo with shared packages and coordinated deployments is simpler and more reliable.
Don't Use It For Tightly Coupled UIs
If your "micro frontends" need to share state extensively, pass dozens of props between each other, or render within the same layout context, they're not really independent — they're components of the same app. Federation adds complexity without benefit.
Don't Use It Without Strong Versioning
Federation's runtime sharing means a breaking change in a remote can crash the host in production — with no build step to catch it. You need:
- Contract testing between host and remote
- Canary deployments for remote updates
- Rollback mechanisms for bad deploys
- Version pinning for critical remotes
Module Federation shares code at runtime over the network. If the remote's CDN goes down, the host app breaks. Unlike npm packages (bundled at build time), federated modules have a runtime dependency on the remote's availability. Always implement fallbacks for when remote modules fail to load:
const RemoteWidget = lazy(() =>
import('remote/Widget').catch(() => import('./FallbackWidget'))
);Don't Use It To Avoid Monorepos
Some teams adopt federation to avoid setting up a monorepo. This is backwards — federation introduces more operational complexity than a monorepo. If your pain point is "sharing code between repos is hard," the answer is a monorepo with turborepo/nx, not federation.
Architecture Patterns
Shell Architecture
The most common pattern: a shell app provides the layout (header, navigation, sidebar) and orchestrates micro-frontends from remotes:
Shell (Host)
├── Header (own code)
├── Navigation (own code)
├── Main Content Area
│ ├── Route: /products → Remote: catalog/ProductPage
│ ├── Route: /checkout → Remote: checkout/CheckoutFlow
│ └── Route: /account → Remote: account/AccountDashboard
└── Footer (own code)
Shared Library Pattern
A central "shared" remote exposes the design system and common utilities. All other apps consume from it:
design-system (Remote)
├── exposes: ./Button, ./Modal, ./Input, ./theme
app-a (Host + Remote)
├── consumes: design-system/Button, design-system/Modal
├── exposes: ./FeatureA
app-b (Host)
├── consumes: design-system/Button, design-system/Input
├── consumes: app-a/FeatureA
| What developers do | What they should do |
|---|---|
| Using Module Federation for a single-team application Federation adds network runtime dependencies, version negotiation, and operational complexity that only pays off when team coordination costs exceed those operational costs. | Use federation only when multiple teams need independent deployment |
| Not implementing fallbacks for remote module loading failures Remote modules are loaded over the network at runtime. CDN downtime, network errors, or deployment issues can prevent loading. Without fallbacks, the host app crashes. | Always wrap remote imports in catch blocks with local fallback components |
| Sharing state between federated micro-frontends via props drilling or shared stores Tight coupling between micro-frontends defeats the purpose of federation. If they need extensive shared state, they should be the same app, not separate federated modules. | Use event-based communication or URL state for cross-micro-frontend communication |
| Setting eager: true on shared dependencies eager: true bundles the shared dependency into the entry chunk, preventing it from being shared with other federated apps. Each app loads its own copy, eliminating the sharing benefit. | Use eager: false (default) to enable runtime sharing |
- 1Module Federation enables runtime code sharing between independently deployed apps — the core value is independent deployment without rebuild coordination.
- 2Shared singleton dependencies (React, frameworks) must be declared with singleton: true to prevent multiple instances.
- 3Always implement fallbacks for remote modules — they depend on network availability, unlike build-time dependencies.
- 4Module Federation 2.0 provides a framework-agnostic runtime, type safety, and a manifest protocol beyond webpack.
- 5Don't use federation for single-team apps or tightly coupled UIs — the operational complexity exceeds the benefits.
- 6Contract testing between host and remote is essential — a breaking change in a remote can crash the host in production with no build step to catch it.