TL;DR / Micro-frontend orchestration composes independently developed and deployed frontend applications into a unified user experience, managed by a shell application that handles routing, communication, and shared concerns.
How It Works
┌──────────────────────────────────────────────┐
│ Shell / Host │
└──────────────────────────────────────────────┘
│
│
┌────────────────└────────────────┐
│ │ │
│ │ │
↓ ↓ ↓
┌────────────┐ ┌────────────┐ ┌────────────┐
│ App A │ │ App B │ │ App C │
│ Team 1 │ │ Team 2 │ │ Team 3 │
└────────────┘ └────────────┘ └────────────┘
┌──────────────────────────────────────────────┐
│ Shared Bus / Events │
└──────────────────────────────────────────────┘
Micro-frontends apply the microservices principle to the frontend: independently developed, tested, and deployed applications composed into a single user-facing product. The orchestration layer is the critical piece that determines whether this architecture produces a seamless experience or a fragmented mess.
The Shell Application
The shell (or host) is the top-level application that owns the page chrome: navigation, authentication, layout skeleton, and routing. It loads micro-frontends into designated regions of the page based on the current route. The shell should be thin -- it contains no business logic, minimal UI beyond the layout, and changes infrequently. Its stability is paramount because every micro-frontend depends on it.
The shell handles route-to-application mapping. When the URL changes, the shell determines which micro-frontend(s) to load, lazy-loads them if necessary, and mounts them into their designated containers. Route ownership must be explicit and non-overlapping -- two micro-frontends claiming the same route creates unresolvable conflicts.
Composition Strategies
Build-time composition (npm packages, monorepo imports) integrates micro-frontends at build time. Each micro-frontend is published as a package and imported by the shell. This provides the best performance (single bundle, tree-shaking) but sacrifices independent deployment -- changing one micro-frontend requires rebuilding and deploying the shell.
Runtime composition via Module Federation (Webpack 5, Rspack) loads micro-frontends as remote JavaScript modules at runtime. Each app exposes components or modules that the shell imports dynamically. This enables true independent deployment -- a team can deploy a new version of their micro-frontend, and the shell picks it up on the next page load. Shared dependencies (React, a design system) are deduplicated through the shared configuration.
Server-side composition assembles the page from micro-frontend HTML fragments at the edge or in an SSR layer. Each micro-frontend renders its HTML independently, and a composition service stitches them into a single response. This works well for content-heavy pages but complicates client-side interactivity.
iframe isolation gives each micro-frontend its own browsing context with complete CSS and JS isolation. Communication happens through postMessage. Iframes prevent style leakage and JavaScript conflicts but create significant UX problems: no shared scrolling, complex deep-linking, inaccessible cross-frame focus management, and large performance overhead from separate JavaScript runtimes.
Inter-Application Communication
Micro-frontends need to communicate without tight coupling. The standard patterns:
Custom Events: Micro-frontends dispatch and listen for custom DOM events. The shell or a shared bus library provides the event bus. Events carry data in their detail property. This is framework-agnostic and decoupled but has no type safety without additional tooling.
Shared state store: A lightweight store (not Redux-scale) managed by the shell that micro-frontends read and subscribe to. Good for shared state like the authenticated user, feature flags, or theme. Must be carefully scoped to avoid coupling micro-frontends to the store's shape.
URL as shared state: The URL is the most durable communication channel. Micro-frontends encode state in URL params, and other micro-frontends read from the URL. Navigation events propagate state changes. This works naturally with browser history and deep-linking.
Shared Dependencies and Deployment
Duplicate React runtimes across micro-frontends destroy performance. Module Federation's shared configuration solves this: the first micro-frontend to load provides React, and subsequent ones reuse it. Design systems must also be shared via a versioned component library to maintain visual consistency.
Each micro-frontend deploys independently to its own CDN path. The shell references them by URL, so deployment is as simple as publishing new assets. Rollbacks are equally simple -- point back to the previous version.
Gotchas
- Shared global state couples micro-frontends just as tightly as a monolith. Keep shared state minimal and well-defined: authenticated user, locale, theme. Business state belongs inside each micro-frontend.
- CSS leakage between micro-frontends is the most common visual bug. Shadow DOM, CSS Modules, or strict naming conventions (BEM with app prefixes) are essential. Global styles in one app will affect all others.
- Performance overhead compounds: each micro-frontend adds its own JavaScript, CSS, and potentially its own framework runtime. Without shared dependency management, bundle sizes can be 3-5x a monolith.
- End-to-end testing across micro-frontends is difficult because each app deploys independently. A contract testing approach (each app tests against agreed interfaces) is more practical than full integration tests.
- Authentication must be centralized in the shell. If each micro-frontend manages its own auth, you get inconsistent login states, token refresh races, and security gaps.