Micro-Frontend Architecture: Scaling Frontend Development Across Teams
Implement micro-frontend patterns that let independent teams build, deploy, and own frontend features without coordinating monolithic releases. Covers Module Federation, single-spa, Web Components, shared design systems, and the organizational patterns that make micro-frontends succeed.
Micro-frontends apply the microservices principle to the frontend: each team owns a vertical slice of the application — from database to UI — and deploys it independently. Team A ships their checkout flow on Tuesday. Team B ships their product catalog on Thursday. Neither team waits for the other. Neither team coordinates a shared release train.
This is not a technology problem. It is an organizational scaling problem. Micro-frontends become valuable when a single frontend codebase has become the bottleneck for multiple teams.
When Micro-Frontends Make Sense
Good Fit
- 3+ teams contributing to the same frontend
- Teams blocked on each other’s release schedules
- Different features have different performance/technology requirements
- Teams need deployment independence
Bad Fit
- Single team building the frontend
- Application is small enough to fit in one codebase
- Consistency across the application is the top priority
- Team does not have the infrastructure maturity to operate independently
Starting with a monolithic frontend and extracting micro-frontends when scaling pain appears is almost always better than starting with micro-frontends.
Integration Patterns
Build-Time Integration (Module Federation)
Webpack Module Federation allows applications to share modules at runtime without bundling them together at build time:
// Host application (shell)
// webpack.config.js
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
});
// Host application - consuming a remote component
const Checkout = React.lazy(() => import('checkout/CheckoutForm'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Checkout orderId={orderId} />
</Suspense>
);
}
Pros: Familiar developer experience, shared bundles reduce duplication, works with existing React/Vue/Angular patterns. Cons: Tight coupling to Webpack/Vite, shared dependency version management.
Runtime Integration (single-spa)
single-spa is a meta-framework that orchestrates multiple frontend applications in the browser:
// root-config.js
registerApplication({
name: '@org/checkout',
app: () => System.import('@org/checkout'),
activeWhen: '/checkout',
});
registerApplication({
name: '@org/catalog',
app: () => System.import('@org/catalog'),
activeWhen: '/products',
});
Each micro-frontend is a fully independent application with its own build pipeline. The root config loads and unloads them based on route.
Pros: Complete technology independence (React + Vue + Angular in one app), independent deployments. Cons: Larger aggregate bundle size, complex routing, CSS isolation challenges.
Server-Side Composition
Services render HTML fragments that are composed on the server:
<!-- Server-assembled page -->
<html>
<body>
<header>
<!-- Fragment from header-service -->
${await fetch('http://header-service/fragment')}
</header>
<main>
<!-- Fragment from product-service -->
${await fetch('http://product-service/fragment?id=123')}
</main>
<aside>
<!-- Fragment from recommendation-service -->
${await fetch('http://recommendation-service/fragment?user=456')}
</aside>
</body>
</html>
Pros: Works with any technology, great for SEO, fast initial page load. Cons: No client-side interactivity between fragments without additional JavaScript.
Web Components
Use native Web Components as the integration boundary:
// Checkout team's Web Component
class CheckoutWidget extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>/* Scoped styles */</style>
<form>...</form>
`;
}
}
customElements.define('checkout-widget', CheckoutWidget);
<!-- Host application -->
<script src="https://checkout.example.com/widget.js"></script>
<checkout-widget order-id="123"></checkout-widget>
Pros: Native browser standard, built-in CSS isolation via Shadow DOM, framework agnostic. Cons: Limited React/Vue/Angular interoperability, Shadow DOM styling limitations.
Shared Concerns
Design System
A shared design system is non-negotiable. Without it, each micro-frontend looks like it was built by a different company:
@org/design-system (npm package)
├── components/
│ ├── Button/
│ ├── Input/
│ ├── Modal/
│ └── ...
├── tokens/
│ ├── colors.css
│ ├── spacing.css
│ └── typography.css
└── themes/
├── light.css
└── dark.css
The design system is published as a versioned npm package. Each team pins to a version and upgrades on their own schedule.
Cross-Cutting Navigation
Navigation, authentication state, and user context must be globally available:
// Shared event bus for cross-micro-frontend communication
const eventBus = {
emit(event, data) {
window.dispatchEvent(new CustomEvent(event, { detail: data }));
},
on(event, callback) {
window.addEventListener(event, (e) => callback(e.detail));
},
};
// Checkout emits when order is placed
eventBus.emit('order:placed', { orderId: '123', total: 299.99 });
// Analytics micro-frontend listens
eventBus.on('order:placed', (data) => {
trackConversion(data.orderId, data.total);
});
Performance Budget
Each micro-frontend gets a performance budget:
Total page budget: 500 KB (compressed)
Shell: 50 KB
Shared design system: 80 KB
Per micro-frontend: 120 KB max
Monitor bundle sizes in CI and fail builds that exceed the budget.
Deployment Independence
The core benefit of micro-frontends — and the hardest to achieve — is independent deployment:
┌────────────────────────────────────────┐
│ CDN / Edge │
├────────────┬────────────┬──────────────┤
│ Shell │ Checkout │ Catalog │
│ v2.1.0 │ v3.4.2 │ v1.8.0 │
│ (Mon) │ (Tue) │ (Thu) │
└────────────┴────────────┴──────────────┘
Each micro-frontend is deployed to its own URL/CDN path. The shell application loads the latest version at runtime. No coordination needed.
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Shared mutable state | Coupling between micro-frontends | Events and props only, no shared global state |
| Inconsistent UX | App feels like 5 different products | Shared design system, design reviews |
| Nano-frontends | Every button is its own micro-frontend | Align boundaries to team/domain ownership |
| Shared database | Backend coupling negates frontend independence | Each team owns their API and data |
| No versioning | Breaking changes cascade | Semantic versioning for shared contracts |
Micro-frontends are an organizational pattern, not a technical one. They succeed when team boundaries align with domain boundaries. They fail when used to solve problems that are better addressed by better communication, shared conventions, or a well-maintained monolith.