Blog/Deep Dives/Micro-Frontends with Module Federation
POST
March 10, 2026
LAST UPDATEDMarch 10, 2026

Micro-Frontends with Module Federation

How to break down monolithic frontend applications into scalable, independently deployable micro-frontends using Webpack's Module Federation.

Tags

ArchitectureReactWebpack
Micro-Frontends with Module Federation
5 min read

Micro-Frontends with Module Federation

TL;DR

Webpack 5's Module Federation lets you split a monolithic frontend into independently deployable applications that load code from each other at runtime. Each team owns a vertical slice of the product -- their own repo, build pipeline, and deployment schedule. The host application dynamically loads remote modules without requiring a rebuild. This guide covers the full configuration, shared dependency management, runtime integration patterns, and the communication strategies that make micro-frontends work in production.

Why This Matters

Frontend monoliths hit a wall. When 15 developers push to the same repository, merge conflicts become daily friction. A CSS change in the checkout flow breaks the product page. The CI pipeline takes 20 minutes because it rebuilds everything. Nobody wants to deploy on Friday because one team's feature is not ready, and it blocks everyone else's.

Micro-frontends solve this the same way microservices solved backend monoliths: by drawing boundaries around business domains and letting teams move independently. The checkout team deploys three times a day. The product catalog team deploys weekly. Neither blocks the other.

Module Federation is the technology that makes this practical. Before it existed, micro-frontend solutions relied on iframes (terrible for UX), build-time package composition (no independent deployability), or custom runtime loaders (complex and brittle). Module Federation is a first-class Webpack feature that handles code sharing, version negotiation, and lazy loading out of the box.

How It Works

The Core Concepts

Module Federation has three roles:

  • Host: The shell application that loads remote modules
  • Remote: An application that exposes modules for others to consume
  • Shared: Dependencies that are shared between host and remotes to avoid duplication

An application can be both a host and a remote simultaneously -- this is called a bidirectional setup.

Configuring the Remote

The remote application exposes specific modules through its Webpack configuration:

javascript
// checkout-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
 
module.exports = {
  output: {
    publicPath: 'https://checkout.example.com/',
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'checkout',
      filename: 'remoteEntry.js',
      exposes: {
        './CheckoutFlow': './src/components/CheckoutFlow',
        './CartSummary': './src/components/CartSummary',
        './useCart': './src/hooks/useCart',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

The filename specifies the manifest file that the host will fetch at runtime. The exposes map defines which modules are available to consumers. The shared configuration prevents loading React twice.

Configuring the Host

The host application declares where to find remote modules:

javascript
// shell-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
 
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
        catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
        account: 'account@https://account.example.com/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

Runtime Integration with React

Loading remote components requires dynamic imports wrapped in React's Suspense and error boundaries:

tsx
// shell-app/src/App.tsx
import React, { Suspense, lazy } from 'react';
import ErrorBoundary from './components/ErrorBoundary';
 
const CheckoutFlow = lazy(() => import('checkout/CheckoutFlow'));
const ProductCatalog = lazy(() => import('catalog/ProductCatalog'));
const AccountDashboard = lazy(() => import('account/Dashboard'));
 
function App() {
  return (
    <div>
      <Navigation />
      <ErrorBoundary fallback={<FallbackUI />}>
        <Suspense fallback={<LoadingSkeleton />}>
          <Routes>
            <Route path="/checkout/*" element={<CheckoutFlow />} />
            <Route path="/products/*" element={<ProductCatalog />} />
            <Route path="/account/*" element={<AccountDashboard />} />
          </Routes>
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

The error boundary is critical. If the checkout service is down, the rest of the application should still work. Without it, a single failed remote takes down the entire page.

Dynamic Remote Loading

For scenarios where remote URLs are not known at build time, you can load remotes dynamically:

typescript
// shell-app/src/utils/loadRemote.ts
async function loadRemoteModule(scope: string, module: string, url: string) {
  await loadScript(url);
 
  const container = window[scope];
  await container.init(__webpack_share_scopes__.default);
  const factory = await container.get(module);
  return factory();
}
 
function loadScript(url: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = () => resolve();
    script.onerror = () => reject(new Error(`Failed to load ${url}`));
    document.head.appendChild(script);
  });
}

This pattern enables configuration-driven architectures where a config service tells the shell which remotes to load based on feature flags, user roles, or A/B test assignments.

Shared Dependency Management

Shared dependencies are the trickiest part of Module Federation. Get them wrong and you either load React twice (doubling bundle size and breaking hooks) or get version conflicts at runtime.

javascript
shared: {
  react: {
    singleton: true,        // Only one copy loaded, ever
    requiredVersion: '^18.0.0',
    eager: false,           // Load lazily (recommended for remotes)
  },
  'react-dom': {
    singleton: true,
    requiredVersion: '^18.0.0',
  },
  '@tanstack/react-query': {
    singleton: true,
    requiredVersion: '^5.0.0',
  },
  // Non-singleton: each remote can use its own version
  'lodash': {
    requiredVersion: '^4.17.0',
  },
}

The rule of thumb: anything that maintains global state (React, state management libraries, routing) must be a singleton. Utility libraries like lodash or date-fns can safely have multiple versions.

Practical Implementation

Communication Between Micro-Frontends

Micro-frontends need to communicate without creating tight coupling. There are three proven patterns:

Custom Events for loose coupling:

typescript
// In the checkout micro-frontend
function publishCartUpdate(cart: Cart) {
  window.dispatchEvent(
    new CustomEvent('cart:updated', { detail: cart })
  );
}
 
// In the header micro-frontend
useEffect(() => {
  const handler = (e: CustomEvent<Cart>) => {
    setCartCount(e.detail.items.length);
  };
  window.addEventListener('cart:updated', handler);
  return () => window.removeEventListener('cart:updated', handler);
}, []);

Shared State via Props for parent-child relationships:

tsx
// Shell passes shared state down to remotes
<CheckoutFlow
  user={currentUser}
  onOrderComplete={(order) => navigate(`/orders/${order.id}`)}
/>

Shared State Store for complex state that spans multiple remotes:

typescript
// shared-state/src/store.ts (deployed as its own federated module)
import { create } from 'zustand';
 
export const useGlobalStore = create((set) => ({
  user: null,
  theme: 'light',
  setUser: (user) => set({ user }),
  setTheme: (theme) => set({ theme }),
}));

Independent Deployment Pipeline

Each micro-frontend gets its own CI/CD pipeline:

yaml
# checkout-app/.github/workflows/deploy.yml
name: Deploy Checkout
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
      - run: npm run build
      - run: aws s3 sync dist/ s3://mfe-checkout/ --delete
      - run: aws cloudfront create-invalidation --distribution-id $CDN_ID --paths "/remoteEntry.js"

Invalidating the remoteEntry.js file on the CDN is essential -- the host application fetches this file at runtime to discover the remote's module map.

Common Pitfalls

Shared dependency version drift. If the checkout team upgrades to React 19 but the shell is still on React 18, the singleton constraint will force one version, potentially breaking the other. Establish a shared dependency contract and enforce it in CI.

CSS conflicts. Without isolation, styles from one micro-frontend leak into another. Use CSS Modules, CSS-in-JS with unique prefixes, or Shadow DOM for strict isolation.

No error boundaries. A single failing remote should not crash the entire application. Every remote import must be wrapped in an error boundary with a meaningful fallback.

Over-decomposition. Not every component needs to be a micro-frontend. If two "micro-frontends" are always deployed together and owned by the same team, they should be one application. The overhead of Module Federation is only justified when you need independent deployability.

Ignoring performance. Each remote adds a network request for its remoteEntry.js manifest and its chunks. Prefetch critical remotes and lazy-load non-critical ones. Monitor the waterfall in your browser DevTools.

When to Use (and When Not To)

Micro-frontends are the right choice when:

  • Multiple teams (3+) work on the same user-facing application
  • Teams need independent release schedules
  • Different parts of the app have different technology requirements
  • The monolithic frontend has become a deployment bottleneck

Stick with a monolith when:

  • You have a small team (fewer than 8 developers)
  • The application is relatively simple
  • You do not have significant deployment coordination problems
  • The added infrastructure complexity is not justified

Consider alternatives when:

  • You just need code sharing (use a component library instead)
  • Your "micro-frontends" are actually separate applications (use separate deployments with shared navigation)
  • You are using Next.js (consider its built-in multi-zone support instead)

FAQ

What is Module Federation in Webpack 5?

Module Federation is a Webpack 5 feature that allows multiple independent builds to share code at runtime, enabling one application to dynamically load components or modules from another deployed application.

When should you adopt micro-frontends?

Micro-frontends are best suited for large organizations with multiple teams working on different business domains of the same application, where independent deployability and team autonomy are more important than simplicity.

How do micro-frontends improve deployment speed?

Each team can deploy their domain (e.g., checkout, product catalog) independently without requiring other teams to rebuild or redeploy, dramatically reducing coordination overhead and deployment risk.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

How to Design API Contracts Between Micro-Frontends and BFFs
Mar 21, 20266 min read
Micro-Frontends
BFF
API Design

How to Design API Contracts Between Micro-Frontends and BFFs

Learn how to design stable API contracts between Micro-Frontends and Backend-for-Frontend layers with versioning, ownership boundaries, error handling, and schema governance.

Next.js BFF Architecture
Mar 21, 20261 min read
Next.js
BFF
Architecture

Next.js BFF Architecture

An architectural deep dive into using Next.js as a Backend-for-Frontend, including route handlers, server components, auth boundaries, caching, and service orchestration.

Next.js Cache Components and PPR in Real Apps
Mar 21, 20266 min read
Next.js
Performance
Caching

Next.js Cache Components and PPR in Real Apps

A practical guide to using Next.js Cache Components and Partial Prerendering in real applications, with tradeoffs, cache strategy, and freshness considerations.