Skip to article
Architecture 19 Jan 2026 14 min read 980 views

Micro-Frontends at Scale: Lessons from a 50-Team Org

Hard-won insights from architecting a micro-frontend platform used by 50 product teams across 3 continents.

Suboor Khan

Full-Stack Developer & Technical Writer

We split a monolithic React application across 50 autonomous product teams serving 3 continents. What followed was 18 months of lessons—some painful—about what micro-frontends are actually good for, where they hurt, and how to make them work at scale.

This isn't a theoretical overview. It's a field report from production, including the mistakes we made and wouldn't make again.

Why Micro-Frontends (and Why Not)

The honest answer: micro-frontends are an organisational pattern, not a technical one. If your teams can coordinate, a monolith is simpler. But when 50 teams need to ship independently without blocking each other, the calculus changes.

  • Good fit: large org, multiple teams, distinct product boundaries, independent deployment requirements
  • Bad fit: small team, shared design system that changes frequently, tight coupling between features

The micro-frontend cost is real: bundle duplication, shared state complexity, cross-app debugging difficulty. Make sure the organisational benefit outweighs it.

Module Federation

Webpack 5 Module Federation lets one app (remote) expose modules that another app (host) consumes at runtime — without rebuilding the host.

// product-app/webpack.config.js (remote)
new ModuleFederationPlugin({
  name:     'product',
  filename: 'remoteEntry.js',
  exposes: {
    './ProductList': './src/components/ProductList',
    './ProductDetail': './src/pages/ProductDetail',
  },
  shared: {
    react:     { singleton: true, requiredVersion: '^18.0.0' },
    'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
  },
})
// shell-app/webpack.config.js (host)
new ModuleFederationPlugin({
  name:    'shell',
  remotes: {
    product: 'product@https://product.suboorkhan.com/remoteEntry.js',
    checkout: 'checkout@https://checkout.suboorkhan.com/remoteEntry.js',
    auth:     'auth@https://auth.suboorkhan.com/remoteEntry.js',
  },
  shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})

50

Teams shipping

0

Forced co-ordination

99.9%

Shell uptime

Cross-App Routing

Each micro-frontend owns its route subtree. The shell maps URL prefixes to remotes. We use a custom event bus for navigation so remotes don't import from the shell.

// Shell routing
const routes = [
  { path: '/products/*',  remote: () => import('product/router') },
  { path: '/checkout/*', remote: () => import('checkout/router') },
  { path: '/account/*',  remote: () => import('account/router') },
];

// Cross-micro-frontend navigation via custom events
window.dispatchEvent(new CustomEvent('mf:navigate', {
  detail: { path: '/checkout/cart', state: { items } }
}));

Shared State Without Coupling

We use a tiny shared event bus package (< 1KB) published to our private npm registry. Session state (user, cart count) is shared via URL params and localStorage only — never direct store access across app boundaries.

// @internal/event-bus (shared singleton)
const bus = {
  emit:  (event, data) => window.dispatchEvent(new CustomEvent(event, {detail:data})),
  on:    (event, fn)   => { window.addEventListener(event, e => fn(e.detail)); },
  off:   (event, fn)   => window.removeEventListener(event, fn),
};

// Contract: auth emits 'user:updated' after login
// Cart listens to 'user:updated' to refresh cart count — no direct imports

Hard-Won Lessons

  • Version-lock your design system. A shared component that changes silently broke 12 teams in one day. Pin versions and use a changelog-enforced bump process.
  • Error isolation is non-negotiable. Each remote must have an ErrorBoundary. One Remote crashing should not kill the shell.
  • Test contracts, not implementations. Use Consumer-Driven Contract Testing (Pact) to validate that host/remote module interfaces are stable without both deploying in the same test env.
  • Cache your remoteEntry.js aggressively. We moved to immutable CDN URLs with content hashes — shells cache the manifest, not the entry point, for instant updates.
  • Shared dependencies are a footgun. singleton: true fails silently if version ranges don't overlap. Add version-compatibility CI checks.

Summary

  • Micro-frontends solve an organisational problem — don't use them for small teams
  • Webpack Module Federation is the mature choice for runtime composition in 2026
  • Route ownership per remote keeps concerns separate — cross-app navigation via events
  • Share as little state as possible — prefer events and URL params over shared stores
  • Error boundaries, contract tests, and immutable CDN URLs are non-negotiable

Stay Updated

Enjoyed this article?

Deep-dive articles on React, AI, WebGL, and software craft — twice a month. No spam, ever.