2025-09-04
Micro Frontend Implementation Patterns: Module Federation and Beyond
Production-ready Module Federation configurations, cross-micro frontend communication, routing strategies, and practical implementation patterns with real debugging examples.
Micro Frontend Series Navigation
- Part 1: Architecture fundamentals and implementation types
- Part 2 (You are here): Module Federation, communication patterns, and integration strategies
- Part 3: Advanced patterns, performance optimization, and production debugging
Prerequisites: This post builds on concepts from Part 1. If you’re new to micro frontends, start there first.
In Part 1, we explored the fundamental architectural patterns for micro frontends. Now we’ll dive deep into practical implementation, focusing on Module Federation as the dominant runtime integration approach, along with real-world communication patterns and debugging strategies I’ve encountered in production systems.
Module Federation Deep Dive
Module Federation, introduced in Webpack 5, has become the gold standard for runtime micro frontend integration. Unlike simple dynamic imports, it provides sophisticated dependency sharing, version management, and runtime composition capabilities.
Setting Up a Production-Ready Module Federation System
Let’s build a realistic e-commerce application with separate teams owning different domains:
// apps/shell/webpack.config.js
// Note: Using stable versions for production reliability rather than bleeding edge.
// @module-federation/enhanced is the newer package (previously @module-federation/webpack)
const ModuleFederationPlugin = require('@module-federation/enhanced');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
remotes: {
// Product team's micro frontend
products: 'products@http://localhost:3001/remoteEntry.js',
// Cart team's micro frontend
cart: 'cart@http://localhost:3002/remoteEntry.js',
// User team's micro frontend
user: 'user@http://localhost:3003/remoteEntry.js',
},
shared: {
// Using stable versions proven in production - not always the latest
// This ensures compatibility across all micro frontends
react: {
singleton: true,
strictVersion: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
strictVersion: true,
requiredVersion: '^18.2.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.8.0',
},
// Custom shared utilities
'@company/design-system': {
singleton: true,
requiredVersion: '^2.1.0',
},
'@company/event-bus': {
singleton: true,
requiredVersion: '^1.0.0',
}
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
devServer: {
port: 3000,
historyApiFallback: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};
// apps/products/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/enhanced');
module.exports = {
mode: 'development',
entry: './src/index.ts',
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail',
'./ProductSearch': './src/components/ProductSearch',
},
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.8.0',
},
'@company/design-system': {
singleton: true,
requiredVersion: '^2.1.0',
},
'@company/event-bus': {
singleton: true,
requiredVersion: '^1.0.0',
}
},
}),
],
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
devServer: {
port: 3001,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};
Robust Module Loading with Error Boundaries
One of the biggest challenges with Module Federation is handling loading failures gracefully. Here’s a production-tested approach:
// src/components/MicroFrontendLoader.tsx
import React, { Suspense, lazy, useState, useEffect } from 'react';
interface MicroFrontendConfig {
scope: string;
module: string;
url: string;
fallback?: React.ComponentType;
}
interface LoadingState {
isLoading: boolean;
error: Error | null;
retryCount: number;
}
const useDynamicScript = (url: string) => {
const [ready, setReady] = useState(false);
const [failed, setFailed] = useState(false);
useEffect(() => {
if (!url) return;
const element = document.createElement('script');
element.src = url;
element.type = 'text/javascript';
element.async = true;
setReady(false);
setFailed(false);
element.onload = () => {
console.log(`Dynamic Script Loaded: ${url}`);
setReady(true);
};
element.onerror = () => {
console.error(`Dynamic Script Error: ${url}`);
setReady(false);
setFailed(true);
};
document.head.appendChild(element);
return () => {
console.log(`Dynamic Script Removed: ${url}`);
document.head.removeChild(element);
};
}, [url]);
return { ready, failed };
};
const loadComponent = (scope: string, module: string) => {
return async () => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
const container = (window as any)[scope]; // or get the container somewhere else
if (!container) {
throw new Error(`Container '${scope}' not found`);
}
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const factory = await (window as any)[scope].get(module);
const Module = factory();
return Module;
};
};
class MicroFrontendErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback: React.ComponentType },
{ hasError: boolean; error: Error | null }
> {
constructor(props: any) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: any) {
console.error('Micro Frontend Error:', error, errorInfo);
// Send to monitoring service
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track('Micro Frontend Error', {
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
}
render() {
if (this.state.hasError) {
const Fallback = this.props.fallback;
return <Fallback />;
}
return this.props.children;
}
}
export const MicroFrontendLoader: React.FC<{
config: MicroFrontendConfig;
props?: Record<string, any>;
}> = ({ config, props = {} }) => {
const { ready, failed } = useDynamicScript(config.url);
const [loadingState, setLoadingState] = useState<LoadingState>({
isLoading: false,
error: null,
retryCount: 0,
});
useEffect(() => {
if (ready && !loadingState.isLoading) {
setLoadingState(prev => ({ ...prev, isLoading: true, error: null }));
}
}, [ready]);
const handleRetry = () => {
if (loadingState.retryCount < 3) {
setLoadingState(prev => ({
...prev,
retryCount: prev.retryCount + 1,
error: null,
isLoading: true,
}));
// Force reload the script
window.location.reload();
}
};
if (failed || (loadingState.error && loadingState.retryCount >= 3)) {
const Fallback = config.fallback || DefaultErrorFallback;
return <Fallback onRetry={handleRetry} />;
}
if (!ready) {
return <LoadingFallback />;
}
const Component = lazy(loadComponent(config.scope, config.module));
return (
<MicroFrontendErrorBoundary fallback={config.fallback || DefaultErrorFallback}>
<Suspense fallback={<LoadingFallback />}>
<Component {...props} />
</Suspense>
</MicroFrontendErrorBoundary>
);
};
const LoadingFallback: React.FC = () => (
<div className="animate-pulse">
<div className="h-4 bg-gray-300 rounded w-3/4 mb-2"></div>
<div className="h-4 bg-gray-300 rounded w-1/2"></div>
</div>
);
const DefaultErrorFallback: React.FC<{ onRetry?: () => void }> = ({ onRetry }) => (
<div className="p-4 border border-red-300 rounded bg-red-50">
<h3 className="text-red-800 font-semibold">Something went wrong</h3>
<p className="text-red-600 text-sm mt-1">
This section couldn't be loaded. Please try refreshing the page.
</p>
{onRetry && (
<button
onClick={onRetry}
className="mt-2 px-3 py-1 bg-red-600 text-white rounded text-sm"
>
Retry
</button>
)}
</div>
);
Cross-Micro Frontend Communication
One of the most challenging aspects of micro frontend architecture is enabling communication between independently developed and deployed applications. Here are the patterns I’ve found most effective:
1. Event-Driven Communication
// @company/event-bus - Shared event bus package
interface EventBusEvent {
type: string;
payload: any;
source: string;
timestamp: number;
}
class EventBus {
private listeners: Map<string, Array<(event: EventBusEvent) => void>> = new Map();
private eventHistory: EventBusEvent[] = [];
private maxHistorySize = 50;
subscribe(eventType: string, callback: (event: EventBusEvent) => void): () => void {
if (!this.listeners.has(eventType)) {
this.listeners.set(eventType, []);
}
this.listeners.get(eventType)!.push(callback);
// Return unsubscribe function
return () => {
const callbacks = this.listeners.get(eventType);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
publish(type: string, payload: any, source: string = 'unknown') {
const event: EventBusEvent = {
type,
payload,
source,
timestamp: Date.now(),
};
// Add to history
this.eventHistory.push(event);
if (this.eventHistory.length > this.maxHistorySize) {
this.eventHistory.shift();
}
// Notify listeners
const callbacks = this.listeners.get(type) || [];
callbacks.forEach(callback => {
try {
callback(event);
} catch (error) {
console.error(`Error in event listener for ${type}:`, error);
}
});
// Debug logging in development
if (process.env.NODE_ENV === 'development') {
console.log(`[EventBus] ${type}:`, payload);
}
}
getHistory(eventType?: string): EventBusEvent[] {
if (eventType) {
return this.eventHistory.filter(event => event.type === eventType);
}
return [...this.eventHistory];
}
// Replay events for late-loading micro frontends
replayEvents(eventType: string, callback: (event: EventBusEvent) => void) {
const pastEvents = this.eventHistory.filter(event => event.type === eventType);
pastEvents.forEach(event => callback(event));
}
}
export const eventBus = new EventBus();
// React hook for easier usage
export const useEventBus = (
eventType: string,
callback: (event: EventBusEvent) => void,
deps: React.DependencyList = []
) => {
useEffect(() => {
const unsubscribe = eventBus.subscribe(eventType, callback);
return unsubscribe;
}, deps);
const publish = useCallback((payload: any, source?: string) => {
eventBus.publish(eventType, payload, source);
}, [eventType]);
return { publish };
};
2. Practical Usage in Micro Frontends
// products/src/components/ProductList.tsx
import React, { useState, useEffect } from 'react';
import { useEventBus } from '@company/event-bus';
interface Product {
id: string;
name: string;
price: number;
image: string;
}
export const ProductList: React.FC = () => {
const [products, setProducts] = useState<Product[]>([]);
const [filters, setFilters] = useState<any>(null);
// Listen for filter changes from search micro frontend
useEventBus('search:filters-changed', (event) => {
setFilters(event.payload);
});
// Listen for cart updates to show feedback
useEventBus('cart:item-added', (event) => {
// Show success notification
showNotification(`${event.payload.productName} added to cart!`);
});
const { publish } = useEventBus('products:product-selected', () => {});
const handleProductClick = (product: Product) => {
publish({
productId: product.id,
productName: product.name,
source: 'product-list',
}, 'products');
};
useEffect(() => {
// Apply filters when they change
if (filters) {
// Filter products logic here
const filtered = applyFilters(products, filters);
setProducts(filtered);
}
}, [filters]);
return (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
{products.map(product => (
<div
key={product.id}
className="border rounded-lg p-4 cursor-pointer hover:shadow-lg"
onClick={() => handleProductClick(product)}
>
<img src={product.image} alt={product.name} className="w-full h-48 object-cover" />
<h3 className="font-semibold mt-2">{product.name}</h3>
<p className="text-gray-600">${product.price}</p>
</div>
))}
</div>
);
};
// cart/src/components/CartButton.tsx
import React, { useState, useEffect } from 'react';
import { useEventBus } from '@company/event-bus';
export const CartButton: React.FC = () => {
const [itemCount, setItemCount] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
// Listen for product additions
useEventBus('products:product-selected', (event) => {
addToCart(event.payload.productId);
});
const { publish } = useEventBus('cart:item-added', () => {});
const addToCart = async (productId: string) => {
try {
// Add to cart logic
const response = await fetch('/api/cart/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId }),
});
if (response.ok) {
const result = await response.json();
setItemCount(prev => prev + 1);
// Trigger animation
setIsAnimating(true);
setTimeout(() => setIsAnimating(false), 500);
// Notify other micro frontends
publish({
productId,
productName: result.productName,
newTotal: result.cartTotal,
}, 'cart');
}
} catch (error) {
console.error('Failed to add to cart:', error);
}
};
return (
<button
className={`relative p-2 bg-blue-600 text-white rounded-full ${
isAnimating ? 'animate-pulse' : ''
}`}
>
<ShoppingCartIcon className="w-6 h-6" />
{itemCount > 0 && (
<span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
{itemCount}
</span>
)}
</button>
);
};
Routing Strategies
Routing in micro frontend architectures requires careful coordination to avoid conflicts and ensure a seamless user experience:
1. Shell-Controlled Routing
// shell/src/App.tsx
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { MicroFrontendLoader } from './components/MicroFrontendLoader';
const App: React.FC = () => {
return (
<BrowserRouter>
<div className="min-h-screen bg-gray-50">
{/* Global navigation */}
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between h-16">
<div className="flex">
<Link to="/" className="flex items-center px-4 text-lg font-semibold">
E-Commerce
</Link>
<div className="flex space-x-8 ml-8">
<Link to="/products" className="flex items-center px-3 py-2 hover:text-blue-600">
Products
</Link>
<Link to="/categories" className="flex items-center px-3 py-2 hover:text-blue-600">
Categories
</Link>
</div>
</div>
{/* User account micro frontend */}
<div className="flex items-center">
<MicroFrontendLoader
config={{
scope: 'user',
module: './UserMenu',
url: 'http://localhost:3003/remoteEntry.js',
}}
/>
</div>
</div>
</nav>
{/* Main content area */}
<main className="max-w-7xl mx-auto px-4 py-8">
<Routes>
<Route path="/" element={<Navigate to="/products" replace />} />
{/* Products micro frontend handles all /products/* routes */}
<Route
path="/products/*"
element={
<MicroFrontendLoader
config={{
scope: 'products',
module: './ProductsApp',
url: 'http://localhost:3001/remoteEntry.js',
}}
/>
}
/>
{/* Cart micro frontend */}
<Route
path="/cart/*"
element={
<MicroFrontendLoader
config={{
scope: 'cart',
module: './CartApp',
url: 'http://localhost:3002/remoteEntry.js',
}}
/>
}
/>
{/* Fallback for unknown routes */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</div>
</BrowserRouter>
);
};
2. Micro Frontend Internal Routing
// products/src/ProductsApp.tsx
import React from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { ProductList } from './components/ProductList';
import { ProductDetail } from './components/ProductDetail';
import { ProductSearch } from './components/ProductSearch';
export const ProductsApp: React.FC = () => {
const location = useLocation();
// Analytics tracking for micro frontend
useEffect(() => {
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.page('Products', {
path: location.pathname,
microfrontend: 'products',
});
}
}, [location.pathname]);
return (
<div className="products-app">
<Routes>
{/* Note: paths are relative to /products */}
<Route index element={<ProductList />} />
<Route path="search" element={<ProductSearch />} />
<Route path="category/:categoryId" element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Routes>
</div>
);
};
A Debugging Story: The Mystery of the Disappearing Routes
Here’s a real debugging challenge that illustrates the complexity of micro frontend routing. We had a production issue where certain product detail pages would randomly show a 404 error, but only for some users and only sometimes.
The investigation revealed a race condition in our routing setup:
// The problematic code
const ProductsApp: React.FC = () => {
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
// This was causing the race condition
setTimeout(() => setIsLoaded(true), 100);
}, []);
if (!isLoaded) {
return <div>Loading...</div>;
}
return (
<Routes>
<Route path=":productId" element={<ProductDetail />} />
</Routes>
);
};
The issue was that React Router in the shell was trying to match routes before the micro frontend had finished initializing its own routes. Users with faster connections would hit the race condition more often.
The solution required coordination between shell and micro frontend routing:
// Fixed version with proper route registration
import { useEventBus } from '@company/event-bus';
const ProductsApp: React.FC = () => {
const [routesReady, setRoutesReady] = useState(false);
const { publish } = useEventBus('routing:micro-frontend-ready', () => {});
useEffect(() => {
// Register available routes with the shell
publish({
microfrontend: 'products',
routes: [
'/products',
'/products/search',
'/products/category/:categoryId',
'/products/:productId'
]
}, 'products');
setRoutesReady(true);
}, [publish]);
if (!routesReady) {
return <LoadingSpinner />;
}
return (
<Routes>
<Route index element={<ProductList />} />
<Route path="search" element={<ProductSearch />} />
<Route path="category/:categoryId" element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Routes>
);
};
This experience taught us the importance of:
- Explicit route registration between shell and micro frontends
- Proper loading states that don’t interfere with routing
- Comprehensive monitoring of route resolution in production
Development and Testing Strategies
Local Development Setup
// scripts/dev-all.js - Script to run all micro frontends locally
const { spawn } = require('child_process');
const path = require('path');
const services = [
{ name: 'shell', port: 3000, path: './apps/shell' },
{ name: 'products', port: 3001, path: './apps/products' },
{ name: 'cart', port: 3002, path: './apps/cart' },
{ name: 'user', port: 3003, path: './apps/user' },
];
const processes = [];
services.forEach(service => {
console.log(`Starting ${service.name} on port ${service.port}...`);
const process = spawn('npm', ['run', 'dev'], {
cwd: path.resolve(service.path),
stdio: 'inherit',
shell: true,
env: { ...process.env, PORT: service.port.toString() }
});
processes.push(process);
});
// Graceful shutdown
process.on('SIGTERM', () => {
processes.forEach(p => p.kill());
});
process.on('SIGINT', () => {
processes.forEach(p => p.kill());
process.exit(0);
});
Integration Testing
// tests/integration/micro-frontend-integration.test.ts
import { test, expect, Page } from '@playwright/test';
test.describe('Micro Frontend Integration', () => {
test('should load all micro frontends correctly', async ({ page }) => {
await page.goto('http://localhost:3000');
// Wait for shell to load
await expect(page.locator('[data-testid="shell-loaded"]')).toBeVisible();
// Check that micro frontends are loaded
await expect(page.locator('[data-testid="products-mf"]')).toBeVisible();
await expect(page.locator('[data-testid="user-menu-mf"]')).toBeVisible();
});
test('should handle micro frontend communication', async ({ page }) => {
await page.goto('http://localhost:3000/products');
// Click on a product
await page.click('[data-testid="product-card"]:first-child');
// Verify cart was updated
await expect(page.locator('[data-testid="cart-count"]')).toContainText('1');
// Verify notification appeared
await expect(page.locator('[data-testid="notification"]')).toContainText('added to cart');
});
test('should handle micro frontend failures gracefully', async ({ page }) => {
// Simulate network failure for one micro frontend
await page.route('**/products/remoteEntry.js', route => {
route.abort();
});
await page.goto('http://localhost:3000');
// Should show fallback UI
await expect(page.locator('[data-testid="products-fallback"]')).toBeVisible();
// Other micro frontends should still work
await expect(page.locator('[data-testid="user-menu-mf"]')).toBeVisible();
});
});
What’s Coming Next
You now have the foundation for implementing micro frontends with Module Federation, along with robust communication and routing patterns. But production systems require even more sophisticated approaches.
Continue to Part 3: Advanced Patterns, Performance, and Debugging for:
- Advanced state management across distributed frontends
- Performance optimization and bundle analysis techniques
- Production debugging stories and monitoring strategies
- Security patterns for cross-origin communication
- Memory leak detection and resolution
- Migration strategies from monoliths
Key Insight: The patterns covered here work well for most use cases, but the real challenges emerge when dealing with scale, complex state interactions, and the inevitable edge cases in production environments.
The debugging stories shared represent common issues you’ll encounter. Every micro frontend system brings unique complexities, but understanding these foundational patterns will help you navigate them more effectively.
Series Navigation
Micro Frontend Architecture Guide
A 3-part comprehensive guide to micro frontend architecture, from fundamental concepts to advanced patterns and production debugging strategies.
All posts in this series
Related posts
Complete guide to micro frontend architectures with real-world implementation patterns, debugging stories, and performance considerations for engineering teams.
A practical comparison of TypeScript AI SDKs for building AI agents - Vercel AI SDK, OpenAI Agents SDK, and AWS Bedrock integration. Includes code examples, decision frameworks, and production patterns.
A comprehensive comparison of modern TypeScript linting and formatting tools - ESLint, Prettier, Biome, and Oxlint - with performance benchmarks, configuration examples, and migration strategies.
Learn how SOLID principles apply to modern JavaScript development. Practical examples with TypeScript, React hooks, and functional patterns - plus when to use them and when they're overkill.
A comprehensive guide to understanding Effect, learning it incrementally, and integrating it with AWS Lambda. Includes real code examples, common pitfalls, and practical patterns from production usage.