Module Federation

TL;DR / Module Federation lets independently built and deployed applications share JavaScript modules at runtime, enabling micro-frontend architectures without duplicating dependencies.

How It Works

 ┌────────────┐          ┌────────────────┐          ┌────────────────┐
 │  Host App  │          │  Remote App A  │          │  Remote App B  │
 └────────────┘          └────────────────┘          └────────────────┘
        │                         │                           │
        └─────────┐               │              ┌────────────┘
                  │               │              │
                  │               │              │
                  │               ↓              │
                  │   ┌──────────────────────┐   │
                  │   │    Shared Runtime    │   │
                  └──→│    (shared deps)     │←──┘
                      └──────────────────────┘


 Each app deploys independently
 Shares modules at runtime

Edit diagram

Module Federation is a webpack 5 feature (also implemented in Rspack and supported through plugins in Vite) that allows multiple independently built JavaScript applications to share code at runtime. Unlike traditional bundling where dependencies are resolved at build time and duplicated across bundles, Module Federation negotiates shared dependencies at load time, enabling a single copy of React or any library to serve multiple applications running on the same page.

The Host-Remote Model

Module Federation uses two roles: hosts and remotes. A host is the application that initiates the loading — typically the shell or container app. A remote is an application that exposes modules for others to consume. These roles are not exclusive; an application can be both a host (consuming modules from others) and a remote (exposing modules to others) simultaneously.

Each remote builds independently and deploys its own bundle to its own URL. It produces a remoteEntry.js file — a manifest that declares what modules are available and how to load them. The host is configured with the URL of each remote entry. At runtime, the host fetches the remote entry, discovers available modules, and loads them on demand through the same chunk-loading mechanism used for local dynamic imports.

Shared Module Negotiation

The most powerful aspect of Module Federation is dependency sharing. When both the host and a remote depend on React 18, there is no reason to download it twice. The shared configuration declares which dependencies should be deduplicated. At runtime, a negotiation protocol determines which application's copy of the dependency to use.

The negotiation follows rules: singleton: true ensures only one version of a library runs (critical for React, which breaks with multiple instances). requiredVersion specifies the semver range the consuming app needs. strictVersion: true throws an error if versions are incompatible rather than silently using a mismatched version. eager: true includes the shared module in the initial bundle rather than loading it asynchronously.

When the host loads first, it registers its version of shared dependencies. When a remote loads, it checks the shared scope. If a compatible version already exists, it uses it. If not, it loads its own copy. This deduplication happens entirely at runtime without any coordination between build pipelines.

Container Interface

Under the hood, each federated application exposes a container — an object with init() and get() methods. init() receives the shared scope (the registry of shared modules) and initializes the remote. get() returns a factory function for a specific exposed module. The host orchestrates this by calling init() on each remote with the current shared scope, then calling get() to retrieve specific modules.

This container interface is what makes Module Federation framework-agnostic. The exposed module could be a React component, a Vue composable, a plain utility function, or a web component. The container does not care about what it contains — it is purely a module loading and sharing protocol.

Version Negotiation Edge Cases

Version management is where Module Federation becomes complex in production. If the host ships React 18.2 and a remote ships React 18.3, the singleton constraint forces one version to win. With strictVersion, the app throws. Without it, the older version typically loads first and the remote silently uses it, which could cause subtle bugs if the remote depends on newer features.

The requiredVersion field should always reflect the actual semver range from each application's package.json. Tooling like @module-federation/enhanced can extract these ranges at build time automatically.

Deployment Architecture

Module Federation shines in micro-frontend architectures where different teams own different features. Team A deploys their dashboard remote independently. Team B deploys their settings remote. The shell app never needs to rebuild when a remote changes — it fetches the latest remoteEntry.js on each page load (or based on cache headers). This decouples deployment cycles entirely, which is the primary organizational benefit.

The cost is operational complexity. You need reliable CDN hosting for remote entries, versioning strategies to handle rollbacks, and monitoring to detect when a remote fails to load. The promise new Promise() pattern in webpack config can be used for dynamic remote resolution — fetching remote entry URLs from a service discovery API rather than hardcoding them.

Gotchas

  • Shared singleton violations — running two copies of React on the same page causes hooks to break, context to fail, and event handling to malfunction. Always set singleton: true for framework dependencies.
  • Async bootstrap required — the host application must bootstrap asynchronously (typically via a dynamic import('./bootstrap')) to allow shared module negotiation to complete before any code executes. Forgetting this causes "Shared module is not available for eager consumption" errors.
  • Version drift in production — if remote A is deployed with React 18.2 and remote B with React 18.3, the first to load wins. This can cause hard-to-reproduce bugs that only appear with specific loading orders.
  • No type safety across boundaries — TypeScript types are not shared at runtime. Changes to a remote's exposed API can silently break the host. Use contract testing or shared type packages to mitigate.
  • Remote entry caching — aggressive HTTP caching on remoteEntry.js prevents users from getting updates. Use cache-busting strategies (hashed filenames or short TTLs) for remote entries while keeping chunk files cached long-term.