Micro Frontend Architecture: Module Federation, Single-SPA & Beyond
Micro frontends extend the microservices pattern to the frontend — each team owns an independent slice of the UI, from database to deploy button.
Why Micro Frontends#
Monolithic Frontend Micro Frontend
┌───────────────────────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ │ │ Team │ │ Team │ │ Team │
│ One giant app │ │ A │ │ B │ │ C │
│ One repo │ → │ React │ │ Vue │ │ Svelte│
│ One deploy │ │ own │ │ own │ │ own │
│ One team backlog │ │ CI/CD │ │ CI/CD │ │ CI/CD │
│ │ └───────┘ └───────┘ └───────┘
└───────────────────────┘ ↓ ↓ ↓
┌───────────────────────────┐
│ Shell / Container App │
└───────────────────────────┘
Benefits#
- Independent deployments — team A ships without waiting for team B
- Technology freedom — each micro frontend can use a different framework
- Autonomous teams — vertical ownership from UI to API
- Isolated failures — a crash in one module does not take down the whole page
- Incremental migration — rewrite one section at a time, not the entire app
Trade-offs#
- Increased complexity — orchestration, routing, shared state
- Bundle duplication — risk of shipping React twice without careful sharing
- Consistency — harder to keep UX uniform without a shared design system
- Testing — integration tests must span multiple micro frontends
Integration Approaches#
1. Module Federation (Webpack 5 / Rspack)#
Module Federation lets separately built and deployed bundles share modules at runtime.
┌─────────────────┐ ┌─────────────────┐
│ Shell App │ │ Checkout MFE │
│ (host) │ ←── │ (remote) │
│ exposes: nav │ │ exposes: │
│ consumes: │ │ CartWidget │
│ CartWidget │ │ CheckoutPage │
└─────────────────┘ └─────────────────┘
Host configuration (shell):
// webpack.config.js — Shell
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.cdn.example.com/remoteEntry.js',
catalog: 'catalog@https://catalog.cdn.example.com/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
})
]
};
Remote configuration (checkout):
// webpack.config.js — Checkout MFE
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: {
'./CartWidget': './src/CartWidget',
'./CheckoutPage': './src/CheckoutPage'
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' }
}
});
Consuming a remote module:
const CartWidget = React.lazy(() => import('checkout/CartWidget'));
function App() {
return (
<React.Suspense fallback="Loading cart...">
<CartWidget />
</React.Suspense>
);
}
2. Single-SPA#
single-spa is a framework-agnostic orchestrator that mounts and unmounts micro frontends based on URL routes.
URL: /dashboard → mount Dashboard MFE
URL: /settings → mount Settings MFE
URL: /checkout → mount Checkout MFE
Each MFE registers lifecycle hooks:
bootstrap → mount → unmount
Registration:
import { registerApplication, start } from 'single-spa';
registerApplication({
name: 'dashboard',
app: () => System.import('@org/dashboard'),
activeWhen: ['/dashboard']
});
registerApplication({
name: 'checkout',
app: () => System.import('@org/checkout'),
activeWhen: ['/checkout']
});
start();
MFE lifecycle:
// dashboard/src/lifecycle.js
export function bootstrap() { /* one-time init */ }
export function mount(props) { /* render into props.domElement */ }
export function unmount(props) { /* cleanup */ }
3. Web Components#
Framework-agnostic by design. Each micro frontend ships as a custom element.
// catalog-mfe/src/index.js
class CatalogWidget extends HTMLElement {
connectedCallback() {
this.innerHTML = '<div id="catalog-root"></div>';
renderCatalogApp(this.querySelector('#catalog-root'));
}
disconnectedCallback() {
unmountCatalogApp(this.querySelector('#catalog-root'));
}
}
customElements.define('catalog-widget', CatalogWidget);
Usage in the shell:
<catalog-widget></catalog-widget>
<checkout-widget cart-id="abc123"></checkout-widget>
4. Iframe Approach#
The simplest but most limited approach. Each MFE runs in a sandboxed iframe.
Pros: Complete isolation, any framework, no JS conflicts
Cons: No shared state, poor UX (scroll, navigation), SEO issues
Best suited for admin panels or third-party embeds where isolation matters more than seamless UX.
Approach Comparison#
| Approach | Isolation | Shared Deps | Framework Freedom | Complexity |
|---|---|---|---|---|
| Module Federation | Medium | Excellent | Any (Webpack-based) | Medium |
| single-spa | Medium | Good | Any | Medium-High |
| Web Components | High | Manual | Any | Low-Medium |
| Iframes | Complete | None | Any | Low |
Shared Dependencies#
Duplicating React, Vue, or large libraries across every MFE bloats the page. Strategies to avoid this:
Singleton Sharing (Module Federation)#
shared: {
react: { singleton: true, eager: false },
'react-dom': { singleton: true, eager: false },
'@org/design-system': { singleton: true }
}
Import Maps (single-spa / native)#
{
"imports": {
"react": "https://cdn.example.com/react@18/index.js",
"react-dom": "https://cdn.example.com/react-dom@18/index.js"
}
}
Version Contract#
All teams agree on major version ranges for shared dependencies. A shared package.json or lockfile enforces this.
Routing#
Shell-Owned Routing#
The shell app handles top-level routes and delegates sub-routes to each MFE:
Shell routes:
/catalog/* → Catalog MFE (owns /catalog/product/:id, /catalog/search)
/checkout/* → Checkout MFE (owns /checkout/cart, /checkout/payment)
/account/* → Account MFE
Communication Between MFEs on Route Change#
// Custom event for cross-MFE navigation
window.dispatchEvent(new CustomEvent('navigate', {
detail: { path: '/checkout/cart', meta: { productId: '123' } }
}));
State Sharing#
Micro frontends should be loosely coupled. Avoid a global Redux store shared across teams.
Recommended Patterns#
| Pattern | Use Case | Coupling |
|---|---|---|
| Custom Events | One-way notifications | Low |
| Shared event bus | Pub/sub between MFEs | Low-Medium |
| URL/query params | Navigation state | Low |
| Shared service (singleton) | Auth, user session | Medium |
| Props/attributes | Parent-to-child data | Low |
Custom Event Example#
// Catalog MFE — dispatch
window.dispatchEvent(new CustomEvent('add-to-cart', {
detail: { productId: '456', quantity: 1 }
}));
// Checkout MFE — listen
window.addEventListener('add-to-cart', (e) => {
addItemToCart(e.detail.productId, e.detail.quantity);
});
Deployment Independence#
Each micro frontend has its own CI/CD pipeline:
Team A pushes to catalog repo
→ Build catalog MFE
→ Upload to CDN (catalog.cdn.example.com/remoteEntry.js)
→ Shell fetches latest remoteEntry.js on next page load
Team B's checkout MFE is unaffected.
Versioning Strategies#
- Latest — shell always fetches the latest remoteEntry.js (simplest)
- Pinned — shell references a specific version URL (safest)
- Canary — deploy to a percentage of users first
Design System Integration#
A shared design system prevents micro frontends from looking like Frankenstein UIs.
┌─────────────────────────────┐
│ @org/design-system │
│ (shared npm package) │
│ Buttons, Forms, Layout, │
│ Tokens, Typography │
└──────────┬──────────────────┘
│ consumed by all MFEs
┌───────┼───────┐
▼ ▼ ▼
Shell Catalog Checkout
Guidelines#
- Publish the design system as a versioned npm package
- Use design tokens (colors, spacing, typography) so MFEs stay consistent
- Mark the design system as a shared singleton in Module Federation
- Run visual regression tests across all MFEs when the design system updates
Common Pitfalls#
- Sharing too much state — tight coupling defeats the purpose of micro frontends
- Duplicate bundles — forgetting to configure shared dependencies doubles page weight
- No design system — each team invents its own buttons and the UI becomes inconsistent
- Over-splitting — not every page needs to be a micro frontend; start with 2-3 boundaries
- Ignoring performance — loading four remoteEntry.js files serially kills page load
- No contract testing — a breaking change in one MFE silently breaks the shell
When to Use Micro Frontends#
Good Fit#
- Large organization with multiple autonomous frontend teams
- Legacy app that needs incremental rewrite
- Platform with distinct product areas (e-commerce: catalog, cart, account)
- Teams with different preferred frameworks
Bad Fit#
- Small team (under 10 developers) — the overhead is not worth it
- Simple app with few pages
- Performance-critical apps where every kilobyte matters
- Team with strong shared codebase and no autonomy issues
Quick-Start Checklist#
[ ] Define team boundaries (which team owns which page/feature)
[ ] Choose an integration approach (Module Federation recommended)
[ ] Set up a shared design system package
[ ] Configure shared dependencies as singletons
[ ] Implement shell-owned routing with delegated sub-routes
[ ] Use custom events for cross-MFE communication
[ ] Give each MFE its own CI/CD pipeline
[ ] Add integration tests that span the shell + MFEs
[ ] Monitor bundle sizes per MFE
Explore the full architecture series on Codelit.io. This is article #348 in our growing library of software architecture and system design guides.
Try these templates
Scalable SaaS Application
Modern SaaS with microservices, event-driven processing, and multi-tenant architecture.
10 componentsNetflix Video Streaming Architecture
Global video streaming platform with adaptive bitrate, CDN distribution, and recommendation engine.
10 componentsSearch Engine Architecture
Web-scale search with crawling, indexing, ranking, and sub-second query serving.
8 componentsContinue learning
Go deeper on system design
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
Building Microservices
Sam Newman · 2021
600 pages on splitting a monolith: service boundaries, data decomposition, migration order.
Learning Domain-Driven Design
Vlad Khononov · 2021
Turns bounded contexts and aggregates into decision rules for where service boundaries actually go.
4.6 (400)Designing Data-Intensive Applications
Martin Kleppmann, Chris Riccomini · 2026
Traces replication, consistency and consensus end to end, with the failure modes behind each choice.
3.6 (196)#1 in Data MiningSoftware Architecture: The Hard Parts
Neal Ford, Mark Richards, Pramod Sadalage, Zhamak Dehghani · 2021
Works through service granularity, distributed data and saga patterns as full trade-off case studies.
4.6 (731)#2 in Software Design Tools