Micro Frontend Architecture: Module Federation, Single-SPA & Beyond
Micro Frontend Architecture#
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 it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Related articles
AI Agent Tool Use Architecture: Function Calling, ReAct Loops & Structured Outputs
6 min read
AI searchAI-Powered Search Architecture: Semantic Search, Hybrid Search, and RAG
8 min read
AI safetyAI Safety Guardrails Architecture: Input Validation, Output Filtering, and Human-in-the-Loop
8 min read
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 componentsBuild this architecture
Generate an interactive Micro Frontend Architecture in seconds.
Try it in Codelit →
Comments