Micro-Frontend Orchestration

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              │
 └──────────────────────────────────────────────┘

Edit diagram

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.