Microfrontend Architecture
Complete guide to designing and implementing microfrontend architectures - patterns, trade-offs, and real-world examples
Microfrontends extend the microservices concept to frontend development, allowing teams to work independently on different parts of a web application.
π― What Are Microfrontends?
Definition
Microfrontends are an architectural pattern where a frontend app is decomposed into individual, independent mini-apps that work together to create a unified user experience.
Core Principles
Traditional Monolith:
βββββββββββββββββββββββββββββββββββββββ
β One Large App β
β βββββββββββββββββββββββββββββββββ β
β β All features in single repo β β
β β One deployment β β
β β One team owns everything β β
β βββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββ
Microfrontends:
βββββββββββββββββββββββββββββββββββββββ
β Shell Application β
β βββββββ βββββββ βββββββ β
β βTeam β βTeam β βTeam β β
β β A β β B β β C β β
β β MFE β β MFE β β MFE β β
β βββββββ βββββββ βββββββ β
β β β β β
β Deploy Deploy Deploy β
β Independently β
βββββββββββββββββββββββββββββββββββββββποΈ Architecture Patterns
1. Build-Time Integration (Simplest)
Each microfrontend is a npm package published independently.
// Shell app package.json
{
"dependencies": {
"@company/header-mfe": "^1.2.0",
"@company/products-mfe": "^2.0.1",
"@company/checkout-mfe": "^1.5.3"
}
}// Shell app
import Header from '@company/header-mfe';
import Products from '@company/products-mfe';
import Checkout from '@company/checkout-mfe';
export default function App() {
return (
<div>
<Header />
<Products />
<Checkout />
</div>
);
}Pros:
- β Simple to implement
- β Type safety
- β Good for small teams
Cons:
- β Must rebuild shell for updates
- β Not truly independent
- β Version conflicts possible
2. Runtime Integration (Module Federation)
Microfrontends are loaded at runtime using Webpack Module Federation.
Setup
// Header MFE - webpack.config.js
const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'header',
filename: 'remoteEntry.js',
exposes: {
'./Header': './src/Header',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};// Shell App - webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
header: 'header@http://localhost:3001/remoteEntry.js',
products: 'products@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};// Shell App - Load dynamically
import React, { lazy, Suspense } from 'react';
const Header = lazy(() => import('header/Header'));
const Products = lazy(() => import('products/ProductList'));
export default function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Header />
<Products />
</Suspense>
</div>
);
}Pros:
- β True independence
- β Deploy without rebuilding shell
- β Share dependencies
- β Load on-demand
Cons:
- β Complex setup
- β Runtime overhead
- β Debugging harder
3. iframe Integration (Isolated)
Each microfrontend runs in an iframe.
export default function App() {
return (
<div>
<iframe
src="https://header.example.com"
style={{ width: '100%', height: '60px', border: 'none' }}
/>
<iframe
src="https://products.example.com"
style={{ width: '100%', height: '800px', border: 'none' }}
/>
</div>
);
}Communication Between iframes:
// Parent (Shell)
window.addEventListener('message', (event) => {
if (event.origin === 'https://products.example.com') {
if (event.data.type === 'PRODUCT_SELECTED') {
console.log('Product selected:', event.data.productId);
}
}
});
// Child (Products MFE)
window.parent.postMessage({
type: 'PRODUCT_SELECTED',
productId: '123'
}, 'https://shell.example.com');Pros:
- β Complete isolation (CSS, JS)
- β Different frameworks
- β Security boundaries
Cons:
- β SEO issues
- β Performance overhead
- β Complex communication
- β Routing difficulties
4. Web Components (Standard)
Use native Web Components for each microfrontend.
// Header MFE - header.ts
class HeaderComponent extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<header>
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
</nav>
</header>
`;
}
}
customElements.define('app-header', HeaderComponent);<!-- Shell App -->
<html>
<body>
<script src="https://header.cdn.com/header.js"></script>
<script src="https://products.cdn.com/products.js"></script>
<app-header></app-header>
<app-products></app-products>
</body>
</html>Pros:
- β Framework agnostic
- β Browser standard
- β Shadow DOM isolation
Cons:
- β Limited React integration
- β SEO challenges
- β Complexity with state
π Routing Strategies
1. Shell-Based Routing (Recommended)
Shell owns the router and decides which MFE to render.
// Shell App
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
const Header = lazy(() => import('header/Header'));
const Products = lazy(() => import('products/App'));
const Checkout = lazy(() => import('checkout/App'));
export default function Shell() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Header />
<Routes>
<Route path="/products/*" element={<Products />} />
<Route path="/checkout/*" element={<Checkout />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}// Products MFE - Internal routing
import { Routes, Route } from 'react-router-dom';
export default function ProductsApp() {
return (
<Routes>
<Route path="/" element={<ProductList />} />
<Route path="/:id" element={<ProductDetail />} />
</Routes>
);
}2. Distributed Routing
Each MFE manages its own routes.
// Shell App
export default function Shell() {
const [currentPath, setCurrentPath] = useState(window.location.pathname);
useEffect(() => {
const handler = () => setCurrentPath(window.location.pathname);
window.addEventListener('popstate', handler);
return () => window.removeEventListener('popstate', handler);
}, []);
return (
<div>
<Header />
{currentPath.startsWith('/products') && <ProductsMFE />}
{currentPath.startsWith('/checkout') && <CheckoutMFE />}
</div>
);
}π¦ Shared Dependencies
Problem: Bundle Duplication
Without sharing:
βββββββββββββββββββββββββββββββββββ
β Header MFE β
β ββ React (42KB) β
β ββ ReactDOM (130KB) β
β ββ App code (20KB) β
βββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββ
β Products MFE β
β ββ React (42KB) β DUPLICATE β
β ββ ReactDOM (130KB) β DUPLICATE β
β ββ App code (80KB) β
βββββββββββββββββββββββββββββββββββ
Total: 444KB (with duplication)Solution: Shared Dependencies
// Both MFEs
new ModuleFederationPlugin({
shared: {
react: {
singleton: true, // Only one instance
requiredVersion: '^18.0.0',
eager: false, // Load on-demand
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
},
},
});With sharing:
βββββββββββββββββββββββββββββββββββ
β Shared Layer β
β ββ React (42KB) β
β ββ ReactDOM (130KB) β
βββββββββββββββββββββββββββββββββββ
β β
β β
ββββββββββ΄ββββ ββββββ΄βββββββββ
β Header MFE β β Products β
β (20KB) β β MFE (80KB) β
ββββββββββββββ βββββββββββββββ
Total: 272KB (38% smaller!)π CSS Isolation
Problem: Style Conflicts
/* Header MFE */
.button {
background: blue;
padding: 10px;
}
/* Products MFE */
.button {
background: red; /* β Conflicts! */
padding: 20px;
}Solution 1: CSS Modules
// Header MFE
import styles from './Header.module.css';
export default function Header() {
return <button className={styles.button}>Click</button>;
}
// Generates: .Header_button__a1b2c3Solution 2: CSS-in-JS with Scoping
import styled from 'styled-components';
// Automatically scoped
const Button = styled.button`
background: blue;
padding: 10px;
`;Solution 3: Shadow DOM (Web Components)
class HeaderComponent extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.button { background: blue; }
</style>
<button class="button">Click</button>
`;
}
}Solution 4: Naming Conventions
/* All Header styles prefixed */
.header-button { }
.header-nav { }
.header-logo { }
/* All Products styles prefixed */
.products-button { }
.products-card { }π State Management
1. Local State (Recommended)
Each MFE manages its own state.
// Products MFE
function ProductsMFE() {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
return (
<ProductsContext.Provider value={{ products, cart }}>
<ProductList />
</ProductsContext.Provider>
);
}2. Shared State via Events
Communication through custom events.
// Custom Event Bus
class EventBus {
private events: Map<string, Function[]> = new Map();
subscribe(event: string, callback: Function) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
}
publish(event: string, data: any) {
const callbacks = this.events.get(event) || [];
callbacks.forEach(cb => cb(data));
}
}
export const eventBus = new EventBus();// Products MFE - Publish
import { eventBus } from '@company/shared';
function addToCart(product) {
eventBus.publish('cart:item-added', { product });
}
// Header MFE - Subscribe
import { eventBus } from '@company/shared';
useEffect(() => {
eventBus.subscribe('cart:item-added', (data) => {
setCartCount(prev => prev + 1);
});
}, []);3. Shared State Store (Use Sparingly)
// Shared store for truly global state
import { create } from 'zustand';
export const useSharedStore = create((set) => ({
user: null,
setUser: (user) => set({ user }),
theme: 'light',
setTheme: (theme) => set({ theme }),
}));// Any MFE can use it
import { useSharedStore } from '@company/shared';
function Header() {
const { user, theme } = useSharedStore();
return <div className={theme}>Welcome {user?.name}</div>;
}π Deployment Strategies
1. Independent Deployments
Each MFE deployed separately.
# Header MFE - GitHub Actions
name: Deploy Header
on:
push:
paths:
- 'packages/header/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: npm run build
- run: aws s3 sync dist/ s3://cdn.example.com/header/CDN Structure:
https://cdn.example.com/
βββ header/
β βββ v1.2.0/
β β βββ remoteEntry.js
β βββ latest/
β βββ remoteEntry.js β v1.2.0
βββ products/
β βββ v2.1.3/
β βββ latest/
βββ checkout/
βββ v1.0.5/
βββ latest/2. Versioned Deployments
Keep multiple versions for rollback.
// Shell dynamically loads versions
const config = {
header: 'https://cdn.example.com/header/v1.2.0/remoteEntry.js',
products: 'https://cdn.example.com/products/v2.1.3/remoteEntry.js',
};
// Easy rollback: just change version
const config = {
header: 'https://cdn.example.com/header/v1.1.9/remoteEntry.js', // Rolled back
products: 'https://cdn.example.com/products/v2.1.3/remoteEntry.js',
};3. Canary Deployments
Gradually roll out new versions.
function getRemoteUrl(mfeName: string) {
const userId = getCurrentUserId();
const isCanary = userId % 100 < 10; // 10% of users
const version = isCanary ? 'v2.0.0-canary' : 'v1.9.0';
return `https://cdn.example.com/${mfeName}/${version}/remoteEntry.js`;
}β οΈ Error Boundaries
Prevent one MFE from crashing the entire app.
// Shell App
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong in this section:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export default function Shell() {
return (
<div>
<Header />
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading Products...</div>}>
<ProductsMFE />
</Suspense>
</ErrorBoundary>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading Checkout...</div>}>
<CheckoutMFE />
</Suspense>
</ErrorBoundary>
</div>
);
}π When to Use Microfrontends
β Good Use Cases
Large Organizations
Scenario: 50+ frontend developers
Problem: Coordination nightmare
Solution: 5 teams Γ 10 devs, each owns MFE
Result: Teams move independentlyDifferent Release Cycles
Team A: Releases daily (experiments)
Team B: Releases monthly (stable features)
Solution: A and B are separate MFEs
Result: No blocking between teamsLegacy Migration
Problem: Rewrite monolith incrementally
Solution: New features as MFEs
Old features: Gradually migrate
Result: No "big bang" rewriteDifferent Tech Stacks
Header: Vue.js (legacy)
Products: React (new)
Checkout: Svelte (experiment)
Solution: Each MFE uses own frameworkβ Bad Use Cases
Small Teams
β 3 developers
β One product
β Overhead > Benefits
β Use monolithTight Coupling
β Features share lots of state
β Constant communication needed
β Can't truly separate
β Use monolith with modulesSimple Applications
β 10 pages total
β Single team
β Unnecessary complexity
β Use regular appπ― Decision Framework
Questions to Ask:
- Team Size: > 20 developers? β Consider MFE
- Independence: Teams can work without coordination? β Consider MFE
- Deploy Frequency: Different cadences? β Consider MFE
- Legacy: Gradual migration needed? β Consider MFE
- Complexity: Can handle distributed systems? β Consider MFE
Trade-offs Table
| Aspect | Monolith | Microfrontends |
|---|---|---|
| Initial Setup | Simple β | Complex β |
| Team Scalability | Hard β | Easy β |
| Performance | Fast β | Overhead β οΈ |
| Consistency | Easy β | Hard β |
| Testing | Simple β | Complex β |
| Debugging | Easy β | Hard β |
| Bundle Size | Optimized β | Duplicates β οΈ |
| Independence | Low β | High β |
π’ Real-World Examples
Spotify
Shell: Navigation + Player
ββ Home MFE (Team Discovery)
ββ Search MFE (Team Search)
ββ Library MFE (Team Library)
ββ Playlist MFE (Team Playlists)
Deploy independently
Share design system
Event-based communicationZalando
Shell: Header + Footer
ββ Product Catalog (Team Catalog)
ββ Shopping Cart (Team Cart)
ββ Checkout (Team Payments)
ββ Account (Team Customer)
iframe-based isolation
Different tech stacks allowed
Gradual migration from monolithAmazon
Shell: Top navigation
ββ Product Page (Team PDP)
ββ Buy Box (Team Buy)
ββ Reviews (Team Reviews)
ββ Recommendations (Team Rec)
Each widget is a MFE
Independent optimization
A/B testing per MFEπ Resources
Tools
- Module Federation: Webpack 5, Vite, Rspack
- Single-SPA: Framework for orchestrating MFEs
- Bit: Component-driven development
- Nx: Monorepo tools for MFEs
Further Reading
π Key Takeaways
- Microfrontends = Microservices for frontend
- Multiple patterns exist (Module Federation, iframes, Web Components)
- Best for large teams with independent release cycles
- Trade complexity for independence
- Not a silver bullet - evaluate carefully
- Start simple - migrate incrementally if needed
Microfrontends solve organizational problems, not technical ones. Use them when team scaling is the bottleneck, not when building faster is the goal.