Back to Blog

Micro-Frontends: Module Federation, Independent Deployment, and Shared State

Implement micro-frontends with Webpack Module Federation — independent deployment, shared dependencies, cross-app state management, and when micro-frontends are

Viprasol Tech Team
May 1, 2026
13 min read

Micro-Frontends: Module Federation, Independent Deployment, and Shared State

Micro-frontends apply the microservices principle to the frontend: instead of one large application, you have multiple independently deployable frontend apps that compose into a single user experience. A team can ship the checkout widget without touching the product catalog, and the navigation bar team can release independently of everyone else.

This solves real problems at large organizations. It also introduces significant complexity that makes no sense for smaller teams. Start with the tradeoffs.


When Micro-Frontends Make Sense

SignalDetails
Multiple teams on one frontend5+ teams merging PRs to the same repo causes constant conflicts
Different release cadencesTeam A ships daily; team B ships monthly — they shouldn't block each other
Different tech stacksLegacy Angular + new React — Module Federation lets them coexist
Organizational autonomyBusiness units want independent ownership and deployment

When they don't make sense:

  • Teams < 20 engineers — shared monorepo with Nx/Turborepo is simpler
  • Greenfield product — you don't know where the seams are yet
  • Single team — independent deployment doesn't help if one team owns everything
  • Tight shared state — frequent cross-app state updates negate isolation benefits

Module Federation: The Core Mechanism

Webpack 5's Module Federation allows a JavaScript bundle to expose modules that other bundles consume at runtime — without bundling them at build time.

Shell App (host)          Product Team (remote)      Cart Team (remote)
├── index.html            ├── remoteEntry.js          ├── remoteEntry.js
├── shell bundle          ├── ProductList component   ├── CartWidget component
└── loads at runtime ──→  └── exposes ProductList     └── exposes CartWidget

The shell loads remote bundles at runtime — updates to ProductList deploy without rebuilding the shell.


🌐 Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript — production-grade stack
  • Fixed-price contracts — no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Setup: Remote App (Exposes Components)

// product-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;

module.exports = {
  mode: 'development',
  devServer: { port: 3001 },
  plugins: [
    new ModuleFederationPlugin({
      name: 'productApp',          // Unique name for this remote
      filename: 'remoteEntry.js',  // Entry point the host loads

      // Expose these modules to other apps
      exposes: {
        './ProductList': './src/components/ProductList',
        './ProductDetail': './src/components/ProductDetail',
      },

      // Share dependencies — prevents loading React twice
      shared: {
        react: {
          singleton: true,         // Only one instance across all apps
          requiredVersion: deps.react,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
  ],
};
// product-app/src/components/ProductList.tsx
// This component will be consumed by the shell

interface Product {
  id: string;
  name: string;
  price: number;
}

interface ProductListProps {
  onAddToCart: (productId: string) => void;
}

export default function ProductList({ onAddToCart }: ProductListProps) {
  const [products, setProducts] = useState<Product[]>([]);

  useEffect(() => {
    fetch('/api/products').then(r => r.json()).then(setProducts);
  }, []);

  return (
    <div className="product-grid">
      {products.map(product => (
        <div key={product.id} className="product-card">
          <h3>{product.name}</h3>
          <p>${(product.price / 100).toFixed(2)}</p>
          <button onClick={() => onAddToCart(product.id)}>
            Add to Cart
          </button>
        </div>
      ))}
    </div>
  );
}

Setup: Shell App (Host — Loads Remotes)

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

module.exports = {
  mode: 'development',
  devServer: { port: 3000 },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',

      // Define remote apps and where to load them from
      remotes: {
        productApp: 'productApp@http://localhost:3001/remoteEntry.js',
        cartApp: 'cartApp@http://localhost:3002/remoteEntry.js',
      },

      shared: {
        react: { singleton: true, requiredVersion: deps.react },
        'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
      },
    }),
  ],
};
// shell/src/App.tsx
import React, { Suspense, lazy } from 'react';

// Lazy load remote components — loaded at runtime, not build time
const ProductList = lazy(() => import('productApp/ProductList'));
const CartWidget = lazy(() => import('cartApp/CartWidget'));

export default function App() {
  const handleAddToCart = (productId: string) => {
    // Cross-app communication via custom events (see shared state section)
    window.dispatchEvent(new CustomEvent('cart:add', { detail: { productId } }));
  };

  return (
    <div className="app">
      <header>
        <h1>My Store</h1>
        <Suspense fallback={<div>Loading cart...</div>}>
          <CartWidget />
        </Suspense>
      </header>

      <main>
        <Suspense fallback={<div>Loading products...</div>}>
          <ProductList onAddToCart={handleAddToCart} />
        </Suspense>
      </main>
    </div>
  );
}

🚀 Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4–8 weeks, full platforms in 3–5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Cross-App State Management

Sharing state between independently deployed apps is the hardest micro-frontend problem. Three patterns:

1. Custom Browser Events (Simple, Decoupled)

// Publish (from product app)
window.dispatchEvent(new CustomEvent('cart:add', {
  detail: { productId: 'prod-123', quantity: 1 },
  bubbles: true,
}));

// Subscribe (in cart app)
useEffect(() => {
  const handler = (e: CustomEvent) => {
    addToCart(e.detail.productId, e.detail.quantity);
  };
  window.addEventListener('cart:add', handler as EventListener);
  return () => window.removeEventListener('cart:add', handler as EventListener);
}, []);

Good for: simple, fire-and-forget events. Bad for: reading shared state (you can't query event history).

2. Shared State Module (via Module Federation)

// shared-state/src/cartStore.ts — exposed as a shared module
import { create } from 'zustand';

interface CartStore {
  items: { productId: string; quantity: number }[];
  addItem: (productId: string, quantity?: number) => void;
  removeItem: (productId: string) => void;
  total: number;
}

// Singleton store — Module Federation + singleton:true ensures
// all apps use the same store instance
export const useCartStore = create<CartStore>((set, get) => ({
  items: [],
  addItem: (productId, quantity = 1) => set(state => ({
    items: [...state.items, { productId, quantity }],
  })),
  removeItem: (productId) => set(state => ({
    items: state.items.filter(i => i.productId !== productId),
  })),
  get total() { return get().items.reduce((sum, i) => sum + i.quantity, 0); },
}));
// webpack.config.js — expose shared state
exposes: {
  './cartStore': './src/cartStore',
},
shared: {
  zustand: { singleton: true },
}

3. URL / Query String (For Navigation State)

For state that should survive navigation: put it in the URL. Both apps can read it independently.


Dynamic Remote URLs (Production)

In production, remote URLs change per environment. Don't hardcode them:

// shell/src/bootstrap.ts
async function loadRemotes() {
  // Fetch remote URLs from a config service or environment
  const config = await fetch('/api/mfe-config').then(r => r.json());
  // config = { productApp: 'https://products.yourapp.com/remoteEntry.js', ... }

  // Dynamically load remotes
  for (const [name, url] of Object.entries(config)) {
    await loadRemoteEntry(name, url as string);
  }
}

async function loadRemoteEntry(name: string, 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 remote: ${name}`));
    document.head.appendChild(script);
  });
}

Error Boundaries for Remote Failures

Remote apps can fail to load (deploy error, CDN issue). Always wrap in error boundaries:

// shell/src/RemoteErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback: ReactNode;
  remoteName: string;
}

interface State { hasError: boolean; error?: Error }

export class RemoteErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false };

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error) {
    console.error(`Remote "${this.props.remoteName}" failed to load:`, error);
    // Report to your error tracking (Sentry, Datadog)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

// Usage
<RemoteErrorBoundary
  remoteName="productApp"
  fallback={<div>Product catalog temporarily unavailable</div>}
>
  <Suspense fallback={<Spinner />}>
    <ProductList onAddToCart={handleAddToCart} />
  </Suspense>
</RemoteErrorBoundary>

Module Federation vs Alternatives

ApproachSetup ComplexityRuntime OverheadBest For
Module FederationHighLow (native browser loading)Large organizations, different release cadences
iframesLowMediumTrue isolation, legacy apps
Nx/Turborepo monorepoLowNoneSingle team, shared deploys acceptable
Web ComponentsMediumLowFramework-agnostic sharing
npm packagesLowNone (build-time)Shared UI libraries, not independent deployment

For most teams: Nx monorepo delivers 80% of the organizational benefits (independent ownership, parallel builds, clear module boundaries) without runtime complexity. Use Module Federation only when independent deployment is a hard requirement.


Working With Viprasol

We design and implement micro-frontend architectures for large engineering organizations — Module Federation configuration, shared state patterns, CI/CD pipelines for independent deployment, and migration strategies from monolithic frontends.

Talk to our team about frontend architecture for growing engineering organizations.


See Also

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms — we build it all with Next.js and React.

Free consultation • No commitment • Response within 24 hours

Viprasol · Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.