Adetayo Akinsanya unkletayo.dev

Micro-Frontend Architecture: Webpack Module Federation, Custom Elements, and Runtime Isolation

Deconstructing independent deployment, Module Federation runtime containers, Shadow DOM encapsulation, and event buses

Part 15 in Series — Catch up on the previous article: Design System Architecture: Tokens, Components, and Themes (Part 14) before diving into this post.

At an enterprise e-commerce organization, 140 developers worked inside a single monolithic Single Page Application (SPA). During a major product launch, a junior developer on the recommendations team merged a small bug that threw an uncaught error during component initialization.

Because the entire application was built as a single monolithic bundle, the error broke the top-level React render tree. The entire site crashed—preventing millions of shoppers from browsing products or completing checkouts for 40 minutes.

Beyond blast-radius concerns, the monolithic codebase created severe delivery bottlenecks: CI builds took 55 minutes, team deployments were coupled to a single weekly release train, and merge conflicts were constant.

Micro-Frontends solve this by extending the microservices paradigm to client-side applications—decomposing a web application into semi-autonomous applications owned and deployed independently by cross-functional feature teams.


1. Architectural Topologies: Monolith vs Micro-Frontends

Monolithic Single Page Application:
[ Checkout Team ] --+
[ Search Team   ] --+---> [ Single Git Repo ] ---> [ 55-Min CI Monolithic Build ] ---> [ Single Deployment ]
[ Profile Team  ] --+

Micro-Frontend Architecture:
[ Search App (Team A)   ] ---> [ Independent CI Pipeline ] ---> [ Deploy App A (S3/CDN) ] --+
[ Checkout App (Team B) ] ---> [ Independent CI Pipeline ] ---> [ Deploy App B (S3/CDN) ] --+-> [ Dynamic Shell ]
[ Profile App (Team C)  ] ---> [ Independent CI Pipeline ] ---> [ Deploy App C (S3/CDN) ] --+

Micro-Frontend Integration Strategies

  1. Build-Time Integration: Shared packages imported via npm. Flaw: Requires rebuilding and re-deploying the entire shell application whenever a sub-package updates.
  2. Server-Side Edge Integration: Micro-frontends assembled at the CDN/Edge layer using Edge Side Includes (ESI) or SSR streaming.
  3. Client-Side Runtime Integration: Micro-frontends loaded dynamically at runtime via Webpack Module Federation or Web Components.

2. Webpack Module Federation Mechanics

Module Federation enables a JavaScript application to execute code loaded dynamically from a completely separate build and deployment origin at runtime, while sharing singleton dependencies (like React or Redux).

[ Host Shell Container ] 
        |
        | 1. Dynamic Script Import ('https://checkout.enterprise.com/remoteEntry.js')
        v
[ Remote Entry Manifest ] 
        |
        | 2. Negotiate Shared Dependencies (React v18 Singleton)
        v
[ Mount Remote Checkout Component in Host DOM ]

Webpack Module Federation Configuration

// Host Application (webpack.config.js)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "host_shell",
      remotes: {
        // Points to dynamically deployed remote entry point
        checkout_app: "checkout_app@https://checkout.enterprise.com/remoteEntry.js"
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.2.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.2.0" }
      }
    })
  ]
};

3. Runtime Isolation: Web Components & Shadow DOM

Module Federation shares JavaScript runtimes, but does not isolate CSS styles or DOM trees. A global CSS rule in a remote micro-frontend can break styles in the host shell.

Web Components provide true browser-native runtime encapsulation via the Shadow DOM:

// Encapsulated Micro-Frontend Web Component
class MicroCartWidget extends HTMLElement {
  constructor() {
    super();
    // Attach isolated Shadow DOM Tree
    const shadow = this.attachShadow({ mode: "closed" });

    shadow.innerHTML = `
      <style>
        /* Styles inside Shadow DOM NEVER leak out to host shell! */
        .cart-box { background: #111; color: #fff; padding: 12px; }
      </style>
      <div class="cart-box">
        <h3>Shopping Cart</h3>
        <button id="checkout-btn">Proceed to Checkout</button>
      </div>
    `;
  }
}

customElements.define("micro-cart-widget", MicroCartWidget);

4. Cross-Micro-Frontend Communication: Decoupled Event Bus

Micro-frontends should never invoke internal methods on sibling micro-frontends directly. Cross-app communication must execute asynchronously via a decoupled Event Bus pattern built on top of standard browser CustomEvent dispatchers.

// Production Micro-Frontend Event Bus Architecture
export class MicroFrontendEventBus {
  public static publish<T>(eventName: string, detail: T) {
    const event = new CustomEvent(eventName, {
      detail,
      bubbles: true,
      composed: true // Allows CustomEvent to cross Shadow DOM boundaries!
    });
    window.dispatchEvent(event);
  }

  public static subscribe<T>(eventName: string, handler: (detail: T) => void): () => void {
    const listener = (event: Event) => {
      const customEvent = event as CustomEvent<T>;
      handler(customEvent.detail);
    };

    window.addEventListener(eventName, listener);
    return () => window.removeEventListener(eventName, listener);
  }
}

// Example Usage across independent teams:
// Team A (Product App) publishes event:
MicroFrontendEventBus.publish("cart:item-added", { id: "p-100", price: 29.99 });

// Team B (Cart App) listens for event:
MicroFrontendEventBus.subscribe("cart:item-added", (data) => {
  console.log("Cart Updated:", data.id);
});

Summary & Key Takeaways

  • Micro-Frontend Architecture: Decouples enterprise applications into independently deployable units owned by autonomous product teams.
  • Module Federation: Loads remote code dynamically over HTTP at runtime while safely sharing singleton framework libraries (react, react-dom).
  • Shadow DOM Isolation: Use Web Components and Shadow DOM (attachShadow()) to achieve strict CSS style and DOM encapsulation.
  • Decoupled Communication: Use a browser-native CustomEvent Event Bus with composed: true to pass messages across micro-frontend boundaries without tight coupling.

References & Further Reading

  1. Webpack Documentation. Module Federation Architecture & Guide. Webpack.
  2. Geers, M. (2020). Micro-Frontends in Action. Manning Publications.
  3. W3C Recommendation. Shadow DOM v1 Specification. W3C Standard.

Up Next in Series →

Part 16: Edge Rendering & Server Components: SSR Hydration, Static Regeneration (ISR), and React Server Components

Continue to Part 16 →