Micro-frontends: Architecture, Patterns, and Production Decisions in 2026
Single monolithic frontends don't scale with multiple independent teams. Micro-frontends enable autonomous teams to develop, deploy, and evolve interfaces without centralized coordination.
Executive summary
Single monolithic frontends don't scale with multiple independent teams. Micro-frontends enable autonomous teams to develop, deploy, and evolve interfaces without centralized coordination.
Last updated: 3/16/2026
Executive summary
The traditional monolithic frontend—a single application codebase served by a centralized team—works well until it doesn't. As multiple independent teams start contributing to the same interface, merge conflicts, blocked deployments, and coupled dependencies make development progressively slower.
Micro-frontends decouple domain interfaces from code boundaries. Each team maintains its own autonomous frontend, deployed independently, and composed at runtime into a unified experience for the user.
In 2026, micro-frontends have evolved from iframe composition experiments to mature patterns based on module federation, intelligent routing, and component composition. Organizations that adopt micro-frontends correctly reduce team conflicts, accelerate time-to-market, and enable multiple teams to work in parallel without manual coordination.
The monolithic frontend problem
Symptoms of single SPA that doesn't scale
1. Blocked deployments
A trivial change in one feature blocks the deployment of the entire application because all teams share the same codebase and deployment cycle.
2. Merge conflicts
Multiple teams modifying the same App.tsx, routes.tsx, or shared component files create frequent merge conflicts that require manual resolution.
3. Shared tech debt
When one team makes a suboptimal technical decision (e.g., obsolete library), all teams inherit that technical debt because they share the same bundle.
4. Inability to evolve independently
Team A cannot modernize their stack (React 18 → 19) without coordinating with Teams B, C, and D, all of which use the same build.
5. Scaled build times
As the monolithic frontend grows, build times scale from minutes to hours, reducing developer productivity.
When monolithic makes sense
Don't introduce unnecessary complexity. Monolithic frontends still make sense when:
- You have a single frontend team (< 10 developers)
- There are no plans to scale to multiple teams
- Your application has low business complexity
- Performance and simplicity are priorities over team flexibility
Micro-frontend composition patterns
Pattern 1: Module Federation (Webpack 5)
Module Federation allows multiple applications to share code at runtime without build coordination.
typescript// app1/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./Checkout': './src/components/Checkout',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// app2/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app2',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// Consuming remote component
import React from 'react';
const ProductList = React.lazy(() => import('app1/ProductList'));
function App() {
return (
<div>
<h1>App 2</h1>
<React.Suspense fallback={<div>Loading...</div>}>
<ProductList />
</React.Suspense>
</div>
);
}Advantages:
- Native code sharing (React, libraries)
- Hot reloading in development
- Independent versioning
- Full TypeScript support
Trade-offs:
- Webpack configuration complexity
- More complex runtime debugging
- Need to manage shared dependency versions
Pattern 2: Single-SPA with Routing
A shell application that routes to different micro-frontends based on URL.
typescript// shell/src/App.tsx
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
interface MicroFrontend {
route: string;
url: string;
module: string;
}
const microFrontends: MicroFrontend[] = [
{
route: '/dashboard',
url: 'http://localhost:3001/remoteEntry.js',
module: './Dashboard',
},
{
route: '/checkout',
url: 'http://localhost:3002/remoteEntry.js',
module: './Checkout',
},
{
route: '/products',
url: 'http://localhost:3003/remoteEntry.js',
module: './ProductList',
},
];
function Shell() {
const [activeRoute, setActiveRoute] = useState<string | null>(null);
useEffect(() => {
const path = window.location.pathname;
const mf = microFrontends.find(mf => path.startsWith(mf.route));
if (mf && activeRoute !== mf.module) {
setActiveRoute(mf.module);
// Dynamically load micro-frontend
const script = document.createElement('script');
script.src = mf.url;
script.async = true;
document.body.appendChild(script);
}
}, [window.location.pathname]);
return (
<BrowserRouter>
<Routes>
{microFrontends.map(mf => (
<Route
key={mf.route}
path={`${mf.route}/*`}
element={<div id={`${mf.module}-mount`} />}
/>
))}
<Route path="*" element={<div>404</div>} />
</Routes>
</BrowserRouter>
);
}Advantages:
- Simple and familiar URL routing
- Each micro-frontend can have its own internal router
- Easy for debugging (each MF has its own dev URL)
Trade-offs:
- Requires maintained shell application
- Coupling between shell and micro-frontends via URL
- Difficulty sharing state between MFs
Pattern 3: Composition via API (BFF)
Frontend fetches components from a composition API.
typescript// composicao-bff/api/compose.ts
import { NextResponse } from 'next/server';
interface ComponentRequest {
id: string;
props: Record<string, any>;
}
export async function POST(req: Request) {
const requests: ComponentRequest[] = await req.json();
// Fetch each component in parallel
const components = await Promise.all(
requests.map(async (request) => {
const response = await fetch(
`http://${request.id}-service:3000/component/${request.id}`,
{
method: 'POST',
body: JSON.stringify(request.props),
}
);
return { id: request.id, html: await response.text() };
})
);
return NextResponse.json({ components });
}
// shell/src/Page.tsx
import { useEffect, useState } from 'react';
interface ComposedPageProps {
layout: string[];
components: string[];
}
function ComposedPage({ layout, components }: ComposedPageProps) {
const [composedHTML, setComposedHTML] = useState<Record<string, string>>({});
useEffect(() => {
// Fetch HTML for each component
fetch('/api/compose', {
method: 'POST',
body: JSON.stringify(components),
}).then(res => res.json())
.then(data => {
setComposedHTML(data.components);
});
}, [components]);
return (
<div>
{layout.map((slot, i) => (
<div key={i} className={`slot-${slot}`}>
<div
dangerouslySetInnerHTML={{ __html: composedHTML[slot] || '' }}
/>
</div>
))}
</div>
);
}Advantages:
- Micro-frontends can be server-rendered
- Facilitates SEO (each MF can control meta tags)
- Technology flexibility (different stacks per MF)
Trade-offs:
- HTTP request overhead
- No shared JavaScript (each MF loads its own bundle)
- Composition complexity
Pattern 4: Web Components
Browser-native pattern for component composition.
typescript// checkout-service/src/CheckoutElement.ts
import { customElements } from 'html-element';
class CheckoutElement extends HTMLElement {
connectedCallback() {
this.render();
}
render() {
this.innerHTML = `
<div class="checkout-container">
<h2>Checkout</h2>
<form id="checkout-form">
<!-- Checkout form -->
</form>
</div>
`;
this.addEventListener('submit', this.handleSubmit.bind(this));
}
handleSubmit(event: Event) {
event.preventDefault();
// Dispatch custom event to shell
this.dispatchEvent(
new CustomEvent('checkout-submit', {
detail: { formData: this.getFormData() },
bubbles: true
})
);
}
getFormData(): Record<string, string> {
const form = this.querySelector('#checkout-form');
const formData = new FormData(form as HTMLFormElement);
return Object.fromEntries(formData.entries()) as Record<string, string>;
}
}
customElements.define('checkout-element', CheckoutElement);
// shell/src/App.tsx
function Shell() {
return (
<div>
<h1>E-commerce Platform</h1>
<checkout-element />
</div>
);
}Advantages:
- Browser-native pattern (framework-agnostic)
- Style sharing via Shadow DOM
- Facilitates MF integration with different technologies
Trade-offs:
- Shadow DOM limits external styling
- Less ergonomic for developers accustomed to React
- More difficult debugging
Shared state management
Pattern 1: Event Bus (Custom Events)
typescript// shared/src/EventBus.ts
class EventBus {
private listeners: Map<string, Set<Function>> = new Map();
on(event: string, callback: Function): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
// Return unsubscribe function
return () => this.listeners.get(event)!.delete(callback);
}
emit(event: string, data: any): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
}
export const eventBus = new EventBus();
// checkout-mf/src/components/Checkout.tsx
import { eventBus } from '@shared/event-bus';
function Checkout() {
const handleCheckoutComplete = (data: any) => {
console.log('Checkout completed:', data);
// Notify other MFs
eventBus.emit('cart:cleared', {});
};
useEffect(() => {
const unsubscribe = eventBus.on('checkout:complete', handleCheckoutComplete);
return unsubscribe;
}, []);
return <div>Checkout</div>;
}
// cart-mf/src/components/Cart.tsx
function Cart() {
const [cartItems, setCartItems] = useState([]);
useEffect(() => {
// Listen for checkout cleared event
const unsubscribe = eventBus.on('cart:cleared', () => {
setCartItems([]);
});
return unsubscribe;
}, []);
return <div>Cart</div>;
}Pattern 2: Shared Context (via Module Federation)
typescript// shared/src/AppContext.tsx
import React, { createContext, useContext } from 'react';
interface AppContextType {
user: User | null;
cart: CartItem[];
setUser: (user: User) => void;
addToCart: (item: CartItem) => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
export function AppProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [cart, setCart] = useState<CartItem[]>([]);
const addToCart = (item: CartItem) => {
setCart(prev => [...prev, item]);
};
return (
<AppContext.Provider value={{ user, cart, setUser, addToCart }}>
{children}
</AppContext.Provider>
);
}
export function useAppContext() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within AppProvider');
}
return context;
}
// checkout-mf/src/components/Checkout.tsx
import { useAppContext } from '@shared/AppContext';
function Checkout() {
const { cart, user } = useAppContext();
function handleCheckout() {
if (!user) {
// User not logged in - redirect to auth MF
window.location.href = '/auth';
return;
}
// Process checkout with shared cart items
processCheckout(cart);
}
return <div>Checkout</div>;
}Code and asset sharing
Pattern 1: Shared Module via Module Federation
typescript// shared/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shared',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Input': './src/components/Input',
'./api': './src/api',
'./types': './src/types',
'./utils': './src/utils',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
}),
],
};
// product-mf/webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'product',
remotes: {
shared: 'shared@http://localhost:3000/remoteEntry.js',
},
}),
],
};
// product-mf/src/components/Product.tsx
import { Button } from 'shared/Button';
import { ProductAPI } from 'shared/api';
function Product({ id }: { id: string }) {
const [product, setProduct] = useState(null);
useEffect(() => {
ProductAPI.getProduct(id).then(setProduct);
}, [id]);
if (!product) return <div>Loading...</div>;
return (
<div>
<h2>{product.name}</h2>
<p>{product.price}</p>
<Button onClick={() => addToCart(product)}>
Add to Cart
</Button>
</div>
);
}Pattern 2: Monorepo with Shared Packages
typescript// monorepo/package.json
{
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build"
}
}
// packages/shared/package.json
{
"name": "@acme/shared",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./Button": "./dist/components/Button.js",
"./api": "./dist/api/index.js",
"./types": "./dist/types/index.js"
}
}
// packages/product-mf/package.json
{
"name": "@acme/product-mf",
"dependencies": {
"@acme/shared": "workspace:*",
"react": "^18.0.0"
}
}
// product-mf/src/components/Product.tsx
import { Button } from '@acme/shared/Button';
import { ProductAPI } from '@acme/shared/api';Routing and navigation
Pattern 1: Cross-MF Routing via Event Bus
typescript// shared/src/RouterService.ts
class RouterService {
navigate(route: string, params?: Record<string, any>): void {
const event = new CustomEvent('navigate', {
detail: { route, params },
bubbles: true
});
window.dispatchEvent(event);
}
}
export const router = new RouterService();
// shell/src/App.tsx
import { router } from '@shared/RouterService';
function Shell() {
useEffect(() => {
// Listen for navigation events
const handleNavigate = (event: Event) => {
const { route, params } = (event as CustomEvent).detail;
window.history.pushState({}, '', route);
// Update active MF based on route
updateActiveMF(route);
};
window.addEventListener('navigate', handleNavigate);
return () => window.removeEventListener('navigate', handleNavigate);
}, []);
return <div>Shell content</div>;
}
// product-mf/src/components/Product.tsx
function Product({ id }: { id: string }) {
const handleViewCart = () => {
// Navigate to cart MF
router.navigate('/cart', { productId: id });
};
return (
<div>
<h2>Product</h2>
<button onClick={handleViewCart}>View Cart</button>
</div>
);
}Pattern 2: Single Router via Shell
typescript// shell/src/App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const routes = [
{ path: '/products/*', mf: 'product-mf' },
{ path: '/cart/*', mf: 'cart-mf' },
{ path: '/checkout/*', mf: 'checkout-mf' },
{ path: '/account/*', mf: 'account-mf' },
];
function Shell() {
const [activeMF, setActiveMF] = useState<string | null>(null);
useEffect(() => {
const path = window.location.pathname;
const route = routes.find(r => path.startsWith(r.path));
if (route && activeMF !== route.mf) {
setActiveMF(route.mf);
// Load script for corresponding MF
const script = document.createElement('script');
script.src = `http://localhost:3000/${route.mf}/remoteEntry.js`;
document.body.appendChild(script);
}
}, [window.location.pathname]);
return (
<BrowserRouter>
<Routes>
{routes.map(route => (
<Route
key={route.path}
path={route.path}
element={<div id={`${route.mf}-container`} />}
/>
))}
</Routes>
</BrowserRouter>
);
}Shared styles and CSS
Pattern 1: Design Tokens via JavaScript
typescript// shared/src/tokens.ts
export const tokens = {
colors: {
primary: '#0066cc',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
typography: {
fontSize: {
xs: '12px',
sm: '14px',
base: '16px',
lg: '18px',
xl: '20px',
},
fontWeight: {
normal: 400,
medium: 500,
bold: 600,
},
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '12px',
},
};
// product-mf/src/components/Button.tsx
import { tokens } from '@shared/tokens';
const buttonStyles = {
base: `
padding: ${tokens.spacing.sm} ${tokens.spacing.lg};
font-size: ${tokens.typography.fontSize.base};
font-weight: ${tokens.typography.fontWeight.medium};
border-radius: ${tokens.borderRadius.md};
border: none;
cursor: pointer;
transition: all 0.2s ease;
`,
primary: `
background-color: ${tokens.colors.primary};
color: white;
`,
secondary: `
background-color: ${tokens.colors.secondary};
color: white;
`,
};
function Button({ variant = 'primary', children }: ButtonProps) {
return (
<button
className={`${buttonStyles.base} ${buttonStyles[variant]}`}
>
{children}
</button>
);
}Pattern 2: CSS Modules Scoped
css/* product-mf/src/components/Product.module.css */
.productContainer {
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.productName {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.productPrice {
color: #0066cc;
font-size: 16px;
font-weight: 500;
}
/* Prefix to avoid conflicts */
:global(.shared-button) {
background-color: var(--primary-color);
color: white;
}Common anti-patterns
Anti-pattern 1: Iframes as only strategy
Problem: Iframes completely isolate MFs, making communication, shared styling, and accessibility difficult.
Consequences:
- Double scroll (iframe + window)
- Difficulty sharing state
- Poor SEO (crawlers don't index content inside iframes)
- Reduced performance (iframe overhead)
Solution: Use Module Federation, Web Components, or composition via API. Reserve iframes only for third-party MFs you don't control.
Anti-pattern 2: Versioning without compatibility
Problem: Updating a shared dependency (React) without coordinating with all MFs.
Consequences:
- Unexpected runtime errors
- Multiple React versions loaded simultaneously
- Undefined behavior
Solution: Use singleton: true in Module Federation and implement compatibility strategies (semver ranges, version adapters).
Anti-pattern 3: Strong coupling via direct DOM access
Problem: MFs access other MFs' DOM directly via document.getElementById.
Consequences:
- Fragility (DOM changes break integration)
- Violation of encapsulation
- Difficult debugging
Solution: Use Event Bus, Context, or well-defined APIs for cross-MF communication.
Implementation decision
Questions to decide for micro-frontends
- Do you have multiple independent teams working on frontend?
- Yes → Micro-frontends can reduce conflicts
- No → Complexity may not be justified
- Do teams need to deploy independently?
- Yes → Micro-frontends with decoupled deployments
- No → Single SPA with unified CI/CD may be sufficient
- Do different teams need different technical stacks?
- Yes → Composition via API or Web Components
- No → Module Federation allows easy React sharing
- Is SEO critical for your application?
- Yes → Prefer SSR-friendly patterns (API composition, Next.js App Router)
- No → Client-side composition is acceptable
Pattern selection matrix
| Requirement | Module Federation | API Composition | Web Components |
|---|---|---|---|
| Code sharing | Excellent | Poor | Poor |
| Independent deployments | Excellent | Excellent | Excellent |
| SSR-friendly | Limited | Excellent | Limited |
| Framework-agnostic | Poor | Excellent | Excellent |
| Dev experience | Good | Fair | Fair |
| Runtime overhead | Low | High | Low |
| Initial complexity | High | Medium | Medium |
Implementation plan in 90 days
Phase 1: Proof of Concept (30 days)
- Identify 2-3 business domains for decomposition
- Implement POC with Module Federation
- Define shared packages (tokens, components, types)
- Validate development and deployment workflow
Phase 2: Pilot with Team (30 days)
- Migrate one complete domain to micro-frontend
- Implement routing and navigation patterns
- Establish review and deployment processes
- Collect developer feedback
Phase 3: Expansion (30 days)
- Migrate additional domains
- Implement cross-MF performance monitoring
- Document patterns and conventions
- Train additional teams
Monitoring and observability
Critical metrics for micro-frontends
1. Loading performance
- TTFB (Time to First Byte) per MF
- FCP (First Contentful Paint) per MF
- TTI (Time to Interactive) per MF
2. Runtime metrics
- MF loading errors
- Cross-MF communication latency
- Number of re-renders per MF
3. Business metrics
- Error rates per MF
- Conversion per MF
- Time to checkout/critical action
Monitoring implementation
typescript// shared/src/MFMonitor.ts
class MFMonitor {
private metrics: Map<string, any> = new Map();
trackMFTiming(mfName: string, metric: string, value: number): void {
const key = `${mfName}:${metric}`;
this.metrics.set(key, value);
// Send to monitoring platform
this.sendToMonitoring('mf_timing', { mf: mfName, metric, value });
}
trackMFError(mfName: string, error: Error): void {
this.sendToMonitoring('mf_error', {
mf: mfName,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
}
private sendToMonitoring(event: string, data: any): void {
// Implementation depends on monitoring platform (DataDog, New Relic, etc.)
if (typeof window !== 'undefined' && (window as any).analytics) {
(window as any).analytics.track(event, data);
}
}
}
export const mfMonitor = new MFMonitor();
// product-mf/src/index.tsx
import { mfMonitor } from '@shared/MFMonitor';
try {
const startLoad = performance.now();
// Load component
const component = await loadProductComponent(id);
const loadTime = performance.now() - startLoad;
mfMonitor.trackMFTiming('product-mf', 'load_time', loadTime);
} catch (error) {
mfMonitor.trackMFError('product-mf', error as Error);
}Conclusion
Micro-frontends in 2026 are a mature architecture for frontend teams that need to scale development with multiple independent teams. Module Federation, API composition, and Web Components offer different trade-offs in code sharing, performance, and operational complexity.
The decision to implement micro-frontends should not be based on technical hype—it should be based on real problems: team conflicts, blocked deployments, and inability to evolve independently. When implemented correctly, micro-frontends decouple teams, reduce time-to-market, and enable multiple technical stacks to coexist in the same unified interface.
Closing practical question: Can your current frontend be developed, tested, and deployed by multiple independent teams without conflicts or blocks?
Need to design or implement micro-frontend architecture to scale your frontend development? Talk to Imperialis about micro-frontend architecture, pattern selection, and production implementation.
Sources
- Module Federation Documentation - Webpack — Official Module Federation documentation
- Micro-frontends - Martin Fowler — Original micro-frontend concepts
- Module Federation 2.0 - Module Federation 2.0 — Specification evolution
- Web Components Documentation - MDN — Browser-native Web Components pattern
- Single-SPA - Single-SPA — Framework for micro-frontend composition