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.
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:
type and optional payload (JSON object).type filter and callback.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:
version field, and micro frontends must tolerate unknown fields.202 Accepted response is received.GET /api/events?since=...) and resume from a last-seen cursor.Common silent failures:
type mismatches: cart:updated vs cart:updated-v2 vs CartUpdated.total vs price vs totalAmount.timestamp field leading to incorrect UI state when events are out of order.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:
ETag or Last-Modified headers.PATCH requests are handled without side effects.409 Conflict with a diff object.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:
userId or tenantId in requests, leading to incorrect state isolation.Best practices:
Cache-Control: max-age=300, must-revalidate on state reads.If-Match and If-Unmodified-Since headers on updates.state:updated events and refresh their local state.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:
[email protected], but the CDN delivers 1.1.3. The remoteEntry.js file includes a version field, and the runtime checks it against the package.json of the consuming app.shared dependencies: the react library is loaded in the host app but not in the remote, leading to React is not defined errors.sharedLib with different versions. The first one sets window.sharedLib, and subsequent loads use the singleton instance, but version-specific code may be duplicated in the bundle.Silent failures:
sharedLib is loaded but Button is not exported: undefined is not a constructor.remoteEntry.js is fetched but not executed: the window.sharedLib object is missing.loadRemoteEntry is called before webpack is ready: loadRemoteEntry is not a function.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:
rel="preload" for critical remote entries.rel="prefetch" for likely-to-be-used micro frontends (e.g., navigation links).rel="subresource" for secondary assets (e.g., CSS, images) that are loaded after the main script.fetchPriority="high" on link elements for critical remote entries.Failure modes:
preload is used but as="script" is missing: the browser downloads the script but doesn't execute it.prefetch is used but not preload: the remote entry is fetched but not ready when the user clicks a navigation link.fetchPriority is set but the link element is inserted late: the high priority is lost.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:
bubbles: true ensures the product:added event reaches the root document.composed: true allows the event to cross shadow DOM boundaries.Failure modes:
customElements.define is called after window.customElements is available but the element is not registered.connectedCallback is called but getAttribute returns null due to typo in attribute name.product:added event is dispatched but not handled due to missing addEventListener in the host app.Best practices:
attributeChangedCallback to react to attribute updates.adoptedCallback for reattachment in different DOM trees.observedAttributes to detect changes in attributes.Micro frontends communicate across boundaries with implicit contracts. Errors often go unnoticed unless explicitly monitored.
Common error patterns:
PATCH request to the state store returns 400 Bad Request but the client does not handle it.remoteEntry.js is not found, or sharedLib is not defined.product-card element is defined but the shadowRoot is not updated.Monitoring:
console.error with structured data in all micro frontends.event:failed, state:update:failed, module:federation:load:failed, and custom:element:render:failed events.correlationId in all HTTP requests and events to trace a user action across services.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:
catch block in a micro frontend is present but not console.error or window.log.fetch call fails but no catch is attached.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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy