Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Sep 15, 2026·14 min read

Module Federation Core: Hosts, Remotes, and the Singleton Problem

Module Federation is a runtime dependency graph, not a deployment trick. Every shared configuration decision is a bet on which module version all remotes will agree to — and getting it wrong produces silent correctness bugs that are nearly impossible to trace in production.

Module Federation Core: Hosts, Remotes, and the Singleton Problem

A micro-frontend is not a smaller app — it is a domain boundary enforced at the deployment layer. If you can't explain the business capability it owns, you haven't drawn the boundary yet.
Part 2 established that Module Federation is dynamic import() with a runtime negotiation layer, and that two independently loaded chunks sharing a library without coordination produces a silent correctness bug. Now we configure the system — and every decision we make in the configuration is a direct consequence of that foundation.
The failure mode this article prevents: copying a Module Federation config from a tutorial, shipping to production with react missing from the shared array, and watching your useContext calls silently return null for logged-in users — only in the composed environment, never in CI.

1. The Architecture: Host and Remote

1.1 Roles and Responsibilities

Module Federation divides applications into two roles:
Remote — An independently deployed application that exposes modules (components, hooks, utilities) for other applications to consume at runtime. The remote has its own build, its own CI/CD pipeline, and its own deployment URL. It does not know who is consuming it.
Host — An application that consumes modules from one or more remotes at runtime. The host declares which remotes it knows about and imports from them using dynamic import(). The host may also be consumed by another host — the roles are not mutually exclusive.
┌─────────────────────────────────────────────────────┐
│  App Shell (Host)                                   │
│  ├── Loads at startup                               │
│  ├── Owns: Navigation, Auth bootstrap, Routing     │
│  └── Dynamically imports from remotes at runtime   │
└─────────┬───────────────────────────────────────────┘
          │ import('checkout/Cart')     │ import('catalog/ProductGrid')
          ▼                            ▼
┌─────────────────┐         ┌─────────────────────┐
│  Checkout Remote │         │  Catalog Remote      │
│  Deployed: CDN A │         │  Deployed: CDN B     │
│  Exposes:        │         │  Exposes:            │
│    ./Cart        │         │    ./ProductGrid     │
│    ./OrderSummary│         │    ./SearchBar       │
└─────────────────┘         └─────────────────────┘
The key property: the Checkout remote and Catalog remote are deployed to separate CDNs on separate schedules. The App Shell does not rebuild when either remote deploys — it fetches the latest version at runtime.

1.2 The ModuleFederationPlugin Anatomy

Every Module Federation application configures the plugin in its build config. Here is a complete, annotated configuration for the Checkout remote using Rspack (the recommended toolchain in 2026):
javascript
// checkout/rspack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack')

module.exports = {
  entry: './src/index.ts',
  output: {
    // Must be unique across all remotes — used by the federation runtime
    // for the shared scope namespace
    uniqueName: 'checkout',
    publicPath: 'auto',  // federation runtime resolves the URL dynamically
  },
  plugins: [
    new ModuleFederationPlugin({
      // The unique name of this remote — referenced by the host
      name: 'checkout',

      // Modules this remote makes available to hosts
      exposes: {
        './Cart':         './src/components/Cart',
        './OrderSummary': './src/components/OrderSummary',
        './useCheckout':  './src/hooks/useCheckout',
      },

      // Shared dependencies — negotiated at runtime with the host
      shared: {
        react: {
          singleton: true,       // Only one React instance allowed in the shared scope
          requiredVersion: '^18.0.0',  // Accept any React 18.x from the host
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
        'react-router-dom': {
          singleton: true,
          requiredVersion: '^6.0.0',
        },
      },
    }),
  ],
}
And the host (App Shell) configuration:
javascript
// shell/rspack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack')

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',

      // Remote declarations — where to find each remote at runtime
      remotes: {
        checkout: 'checkout@https://cdn.example.com/checkout/remoteEntry.js',
        catalog:  'catalog@https://cdn.catalog.example.com/remoteEntry.js',
      },

      shared: {
        react:        { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom':  { singleton: true, requiredVersion: '^18.0.0' },
        'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' },
      },
    }),
  ],
}
Crucial Requirement
publicPath: 'auto' is required for Module Federation to resolve chunk URLs correctly when the remote is served from a CDN. Without it, chunk requests use the host's origin instead of the remote's — producing 404s for all remote assets.

2. The Request Lifecycle: From Import to Render

When the App Shell encounters import('checkout/Cart') for the first time, the following sequence executes:
Flow trace diagram of a Module Federation remote import request lifecycle. Eight sequential steps shown as horizontal pipeline stages, left to right, with time annotations. Stage 1 (dim): 'App Shell renders route /checkout'. Stage 2 (cyan): 'React.lazy() triggers import("checkout/Cart")'. Stage 3 (cyan): 'Federation runtime checks shared scope — is checkout registered?'. Stage 4 (amber): 'No: fetch remoteEntry.js from CDN (GET https://cdn.example.com/checkout/remoteEntry.js)'. Stage 5 (amber): 'remoteEntry.js evaluates — registers module map and shared deps'. Stage 6 (green): 'Runtime negotiates shared deps: react@18.2.0 already in host scope — reuse'. Stage 7 (green): 'Fetch checkout/Cart chunk (GET /checkout/Cart.js)'. Stage 8 (green): 'Promise resolves — Cart component available — React renders'. Below stages 4 and 7: red annotations 'Network request — can fail'. Between stages 5 and 6: cyan annotation 'Singleton negotiation happens here — this is what shared config controls'. Caption: 'Every remote component load involves at minimum two network requests: the remoteEntry manifest and the component chunk — both can fail and must be handled by Error Boundaries.'
Every remote component load involves at minimum two network requests: the remoteEntry manifest and the component chunk — both can fail and must be handled by Error Boundaries.
This sequence has several implications for production systems:
  1. Two network requests minimumremoteEntry.js + the component chunk. Both can fail. Both can be cached. Neither is guaranteed to be fast.
  2. Singleton negotiation happens at step 6 — this is where shared configuration is evaluated. If react is already in the host's shared scope, the remote reuses it. If not, the remote loads its own copy — which is the bug from Part 2.
  3. The first import is slower than subsequent onesremoteEntry.js is cached after the first load. Subsequent import('checkout/Cart') calls skip steps 3–5 and go directly to step 7 if the chunk is also cached.

3. The Singleton Problem in Depth

3.1 The Three shared Configuration Strategies

The shared configuration field is not just a list of libraries to share. Each entry is a negotiation contract between the host and remote. There are three key options:
javascript
shared: {
  // Strategy 1: Loose sharing — use the highest version found, but allow fallback
  // Use when: library is stateless (lodash, date-fns, zod)
  'date-fns': {
    requiredVersion: '^3.0.0',  // "I need at least 3.0.0"
    // No singleton — multiple versions can coexist harmlessly
  },

  // Strategy 2: Singleton — only one instance, version negotiated at runtime
  // Use when: library has module-level state (React, React Router, Zustand)
  react: {
    singleton: true,
    requiredVersion: '^18.0.0',
    // If host has 18.2.0 and remote requires ^18.0.0 → reuse 18.2.0
    // If host has 17.0.0 and remote requires ^18.0.0 → warning, loads 18.x from remote
  },

  // Strategy 3: Strict singleton — version mismatch is a hard error, not a warning
  // Use when: version mismatch would cause runtime incompatibility (React 17 vs 18 hooks API)
  react: {
    singleton: true,
    strictVersion: true,        // version mismatch → throws at runtime, not warning
    requiredVersion: '~18.2.0', // "I require exactly 18.2.x, patch versions only"
  },
}
StrategyWhen to UseVersion Mismatch Behavior
No singletonStateless utility librariesEach consumer loads its own version — no coordination
singleton: trueStateful libraries with module-level singletonsWarning in console; host version is used
singleton + strictVersionLibraries with breaking API changes between versionsHard runtime error — prevents silently wrong behavior
Performance / Safety Warning
singleton: true without strictVersion: true will silently use the host's React version even if the remote requires an incompatible one. A remote built for React 18 hooks loaded into a React 17 host will call hooks that do not exist — producing runtime errors that are difficult to trace to a version mismatch. Use strictVersion: true for React.

3.2 What Gets Shared vs. What Gets Bundled Locally

Not every node_modules package should be shared. The decision rule is straightforward:
javascript
shared: {
  // ✅ SHARE: Libraries with module-level state (singletons)
  react:            { singleton: true },
  'react-dom':      { singleton: true },
  'react-router-dom': { singleton: true },
  zustand:          { singleton: true },

  // ✅ SHARE: Large, stable libraries that don't change per remote
  // (saves bandwidth — only loaded once across all remotes)
  'date-fns':       { requiredVersion: '^3.0.0' },

  // ❌ DO NOT SHARE: Domain-specific libraries owned by one remote
  // These belong in the remote's local bundle
  // '@stripe/react-stripe-js': — only Checkout uses this
  // 'react-dnd': — only Catalog uses this
  // '@sentry/react': — each remote should have its own Sentry config
}
Sharing a domain-specific library creates an implicit coupling between remotes. If the Checkout remote upgrades @stripe/react-stripe-js to v3, and the shell's shared entry points to v2, the version negotiation produces an unexpected result. Keep domain-specific dependencies in each remote's local bundle.

4.1 Why Rspack in 2026

Rspack is a Rust-based webpack-compatible bundler developed by ByteDance. It is the recommended choice for new MFE projects in 2026 for two reasons:
Performance: Rspack's Rust core delivers 5–10× faster cold builds compared to webpack 5 on the same configuration. For a multi-remote MFE system where each remote builds independently, this compounds — eight remotes each saving 30 seconds is four minutes per CI run.
First-class Module Federation 2.0: Rspack ships @module-federation/enhanced support out of the box. The webpack 5 MF plugin and the Rspack MF plugin share the same runtime and the same configuration schema — migration from webpack 5 to Rspack requires changing two lines:
javascript
// webpack.config.js (before)
const { ModuleFederationPlugin } = require('webpack').container

// rspack.config.js (after) — same config, different import
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack')
// ↑ The rest of the config is identical

4.2 The @module-federation/vite Caveat

javascript
// vite.config.ts — NOT recommended for production MFE systems in 2026
import federation from '@module-federation/vite'

export default {
  plugins: [
    federation({
      name: 'checkout',
      exposes: { './Cart': './src/Cart' },
      shared: { react: { singleton: true } },
    })
  ]
}
Performance / Safety Warning
@module-federation/vite does not support the full Module Federation 2.0 feature set as of 2026. Specifically: @module-federation/dts-plugin (type sharing, Part 4) is not compatible, runtime plugins API is partially implemented, and the shared scope negotiation has known edge cases with singleton: true in development mode. If your team prefers Vite for local development, use Rspack for production builds. Document this decision in your architecture decision record.

5. Design System as a Worked Example

5.1 The Shared Component Library Problem

Every MFE system eventually asks: how do we share a UI component library (Button, Input, Modal, Table) across all remotes while maintaining visual consistency?
There are two architectural choices, with fundamentally different trade-off profiles:
Option A: Singleton Remote — The design system is deployed as its own remote and exposed to all consumers:
javascript
// design-system remote
exposes: {
  './Button': './src/Button',
  './Modal':  './src/Modal',
  './Table':  './src/Table',
}

// Each consuming remote
remotes: {
  'design-system': 'ds@https://cdn.example.com/design-system/remoteEntry.js'
}

// Usage in checkout remote
import Button from 'design-system/Button'
Option B: Versioned NPM Package — The design system is published to npm and each remote installs the version it needs:
bash
# Each remote independently installs a version
cd checkout && npm install @example/design-system@2.1.0
cd catalog  && npm install @example/design-system@2.0.0  # can be on older version
CriterionSingleton RemoteVersioned NPM Package
Visual consistency✅ Always latest — all remotes use the same version❌ Remotes may be on different versions
Independent upgrades❌ All remotes get updates simultaneously✅ Each remote upgrades on its own schedule
Deployment coupling❌ Design system release affects all remotes✅ No coupling
Bundle size✅ Loaded once, shared across all remotes❌ Each remote bundles its own copy
Rollback scope❌ Rolling back affects all remotes✅ Isolated rollback per remote
Comparison diagram of Singleton Remote vs. Versioned NPM Package for shared design system distribution. Left panel shows Singleton Remote architecture (central remote app deployed to CDN, all remotes load runtime chunk, high visual consistency but coupled deploys). Right panel shows Versioned NPM Package architecture (npm registry, independent version pins per remote, zero runtime dependency but possible visual divergence). Caption: 'Sharing UI components as a singleton remote guarantees visual consistency but couples deployment release trains — versioned NPM packages isolate blast radius.'
Sharing UI components as a singleton remote guarantees visual consistency but couples deployment release trains — versioned NPM packages isolate blast radius.
Pro Tip & Optimization
The correct choice depends on your team's release culture. If your design system team releases frequently and breaking changes are rare, a singleton remote gives visual consistency with no duplication cost. If your design system has breaking changes on minor versions, a versioned NPM package with each remote upgrading independently is safer. Most mature organizations use the NPM approach for stability.

Summary

ConceptRule
RemoteExposes modules via exposes config; deployed independently
HostConsumes remotes via remotes config; never rebuilds when remotes deploy
singleton: trueAllows only one instance in the shared scope; host version wins on conflict
strictVersion: trueVersion mismatch is a hard runtime error, not a warning
Share what is statefulReact, Router, Zustand — always shared; domain libs — always local
Rspack vs webpackSame MF config, 5–10× faster builds; prefer Rspack for new projects
@module-federation/viteNot production-stable for complex shared graphs in 2026

What's Next

In Part 4, we upgrade to Module Federation 2.0 — replacing hardcoded remote URLs with CDN-hosted manifests, adding cross-boundary TypeScript safety via @module-federation/dts-plugin, and intercepting the federation lifecycle with runtime plugins for circuit-breaking and telemetry. Part 4 → Module Federation 2.0: Dynamic Remotes, Type Safety, and Runtime Plugins

References

  1. Rspack — Module Federation
  2. Module Federation — Shared Modules
  3. @module-federation/enhanced — GitHub
  4. Webpack 5 — Module Federation
  5. Module Federation Examples — GitHub
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Module Federation#Webpack#Rspack#Micro-Frontends#React#Dependency Management
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.