Micro Frontend Communication

Production engineering guide for micro frontend communication covering patterns, implementation strategies, and operational best practices.

Micro frontends enable teams to independently develop, deploy, and scale frontend applications within a larger web platform. Communication between these independently managed parts—ranging from shared UI components to full sub-applications—is essential to maintain a cohesive user experience. This page details how to build reliable, performant, and maintainable communication between micro frontends using established patterns and real tools.

Shared State via Global Event Bus

When micro frontends need to react to events across boundaries—such as a navigation change in one app triggering a data refresh in another—use a global event bus. The event bus is typically implemented as a lightweight in-memory or distributed message queue, exposing a standard API over HTTP or WebSockets.

The event bus must support:

Example: Publish an event from a cart micro frontend.

// cart-app.js
const eventBus = new EventBus('https://events.example.com/api/events');

eventBus.publish({
  type: 'cart:updated',
  payload: {
    items: 3,
    total: 129.99,
    timestamp: '2024-05-14T10:32:15Z'
  }
});

Example: Subscribe in a checkout micro frontend.

// checkout-app.js
const eventBus = new EventBus('https://events.example.com/api/events');

eventBus.subscribe('cart:updated', (event) => {
  const { items, total } = event.payload;
  document.getElementById('checkout-total').textContent = total;
  document.getElementById('cart-count').textContent = items;
});

The event bus must handle:

Common silent failures:

Shared State via Global Store (State Management)

For complex, synchronized state across multiple micro frontends—such as user session, theme preference, or active tab—use a shared state store, typically a REST or GraphQL API.

Each micro frontend reads and updates the store using standard HTTP methods. The store is versioned and supports optimistic updates and conflict resolution.

Example: Update user theme in a shared store.

PATCH /api/state/user-theme HTTP/1.1
Host: state.example.com
Content-Type: application/json

{
  "theme": "dark",
  "lastUpdated": "2024-05-14T10:32:15Z",
  "userId": "u123"
}

Example: Read theme in a navigation micro frontend.

GET /api/state/user-theme?userId=u123 HTTP/1.1
Host: state.example.com
Accept: application/json

Response:

{
  "theme": "dark",
  "lastUpdated": "2024-05-14T10:32:15Z",
  "userId": "u123",
  "etag": "a3b4c5d6"
}

The store must support:

Example conflict response:

HTTP/1.1 409 Conflict
Content-Type: application/json
ETag: "a3b4c5d6"

{
  "conflict": {
    "current": {
      "theme": "light",
      "lastUpdated": "2024-05-14T10:25:00Z"
    },
    "proposed": {
      "theme": "dark",
      "lastUpdated": "2024-05-14T10:32:15Z"
    },
    "conflictFields": ["theme"]
  }
}

Failure modes:

Best practices:

Shared Components via Module Federation

When micro frontends share UI components—such as a common button, form, or layout—use module federation to expose and consume components dynamically.

Module federation is a webpack feature that enables a single build output to act as both an entry point and a library. It uses a module-federation.js configuration file at the root of the build.

Example: Expose a Button component in the shared library.

// shared-lib/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  entry: './src/index.js',
  output: {
    path: __dirname + '/dist',
    filename: 'shared-lib.js',
    publicPath: 'https://cdn.example.com/shared/'
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'sharedLib',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button.js',
        './Card': './src/Card.js'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
};

Example: Consume the Button component in a product micro frontend.

// product-app/src/index.js
import { Button } from 'sharedLib/Button';

const app = document.getElementById('app');
app.innerHTML = '<button id="btn"></button>';

const btn = new Button({
  label: 'Add to cart',
  onClick: () => alert('Added!')
});
btn.render('#btn');

Example: Load remoteEntry.js dynamically.

<script type="module">
  import { loadRemoteEntry } from 'https://cdn.example.com/shared/remoteEntry.js';
  loadRemoteEntry('sharedLib', 'https://cdn.example.com/shared/');
</script>

Failure modes:

Silent failures:

Dynamic Loading with Resource Hints

To improve performance, micro frontends must load their remote assets with appropriate resource hints.

Example: Preload and prefetch remote entry files.

<link rel="preload" as="script" href="https://cdn.example.com/shared/remoteEntry.js" />
<link rel="prefetch" as="script" href="https://cdn.example.com/product/remoteEntry.js" />
<link rel="prefetch" as="script" href="https://cdn.example.com/cart/remoteEntry.js" />
<link rel="preload" as="script" href="https://cdn.example.com/product/main.js" />
<link rel="subresource" href="https://cdn.example.com/product/main.js" />

Best practices:

Failure modes:

Inter-App Communication via Custom Elements

When micro frontends need to embed one another as self-contained units—such as embedding a product detail view inside a dashboard—use custom elements.

Custom elements are web components defined using class extends HTMLElement, registered with customElements.define.

Example: Define a product-card custom element.

// product-card.js
class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        .card { border: 1px solid #ccc; padding: 1rem; margin: 1rem; }
        button { background: #0077cc; color: white; padding: 0.5rem; }
      </style>
      <div class="card">
        <h3 id="title"></h3>
        <p id="description"></p>
        <button id="add-btn">Add to Cart</button>
      </div>
    `;
  }

  connectedCallback() {
    const title = this.getAttribute('title');
    const description = this.getAttribute('description');
    const price = this.getAttribute('price');

    this.shadowRoot.getElementById('title').textContent = title;
    this.shadowRoot.getElementById('description').textContent = description;
    this.shadowRoot.getElementById('add-btn').addEventListener('click', () => {
      const event = new CustomEvent('product:added', {
        detail: { title, price },
        bubbles: true,
        composed: true
      });
      this.dispatchEvent(event);
    });
  }
}

customElements.define('product-card', ProductCard);

Example: Use the product-card in the dashboard micro frontend.

<product-card
  title="Wireless Earbuds"
  description="High-fidelity audio with noise cancellation."
  price="99.99"
  data-product-id="p101"
></product-card>

Event propagation:

Failure modes:

Best practices:

Error Handling and Observability

Micro frontends communicate across boundaries with implicit contracts. Errors often go unnoticed unless explicitly monitored.

Common error patterns:

Monitoring:

Example: Error log entry.

{
  "timestamp": "2024-05-14T10:32:15Z",
  "level": "error",
  "service": "cart-app",
  "event": "state:update:failed",
  "correlationId": "c12345",
  "request": {
    "method": "PATCH",
    "url": "/api/state/user-theme",
    "payload": {
      "theme": "dark"
    }
  },
  "response": {
    "status": 400,
    "body": {
      "error": "invalid_payload",
      "message": "theme must be string",
      "details": {
        "path": ["theme"],
        "expected": "string",
        "received": "number"
      }
    }
  }
}

Failure modes:

Summary: When to Use What

Each pattern succeeds when it is used consistently, and fails silently when assumptions are violated. The key is to make the communication contract explicit—via documentation, versioning, and observability—and to treat each micro frontend as a first-class citizen in a distributed system.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.