Micro-Frontends: Architecture and Implementation Patterns
Build micro-frontends with Webpack Module Federation and Single-SPA. Covers team organization, shared dependencies, and production deployment patterns.
Introduction
Micro-frontends extend the microservices concept to the frontend. When your frontend team grows beyond 8-10 developers, or when different teams own different parts of the user experience, micro-frontends can enable independent development and deployment.
When to Consider Micro-Frontends
Before implementing, honestly assess if you need them:
Good candidates:
- Large teams (20+ frontend developers)
- Distinct business domains with separate ownership
- Need for independent deployment cycles
- Legacy modernization (strangler pattern)
Poor candidates:
- Small teams wanting to "be like Netflix"
- Simple applications
- Teams without strong DevOps capabilities
Module Federation Architecture
Webpack 5's Module Federation is the most mature approach:
// Host application (shell) webpack.config.js const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin; module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'shell', remotes: { catalog: 'catalog@http://localhost:3001/remoteEntry.js', checkout: 'checkout@http://localhost:3002/remoteEntry.js', account: 'account@http://localhost:3003/remoteEntry.js', }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, 'react-router-dom': { singleton: true, requiredVersion: '^6.0.0' }, }, }), ], }; // Remote application (catalog) webpack.config.js module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'catalog', filename: 'remoteEntry.js', exposes: { './ProductList': './src/components/ProductList', './ProductDetail': './src/pages/ProductDetail', }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, }, }), ], };
Loading Remote Components
// Shell application import React, { Suspense, lazy } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; const ProductList = lazy(() => import('catalog/ProductList')); const Checkout = lazy(() => import('checkout/CheckoutFlow')); function App() { return ( <ErrorBoundary fallback={<div>Failed to load module</div>}> <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/products" element={<ProductList />} /> <Route path="/checkout" element={<Checkout />} /> </Routes> </Suspense> </ErrorBoundary> ); }
Shared Dependencies Strategy
Managing shared dependencies is crucial for bundle size:
// shared.config.js - used by all micro-frontends const sharedDependencies = { react: { singleton: true, requiredVersion: '^18.0.0', eager: false, }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0', eager: false, }, // Design system must be singleton to ensure consistent styling '@company/design-system': { singleton: true, requiredVersion: '^2.0.0', }, // Utility libraries can be shared but don't need singleton lodash: { singleton: false, requiredVersion: '^4.17.0', }, };
Cross-Micro-Frontend Communication
Event Bus Pattern
// Shared event bus type EventCallback = (data: unknown) => void; class EventBus { private listeners: Map<string, Set<EventCallback>> = new Map(); subscribe(event: string, callback: EventCallback): () => void { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event)!.add(callback); return () => { this.listeners.get(event)?.delete(callback); }; } publish(event: string, data: unknown): void { this.listeners.get(event)?.forEach(callback => callback(data)); } } // Singleton instance shared across micro-frontends export const eventBus = new EventBus(); // Usage in catalog micro-frontend function ProductCard({ product }: { product: Product }) { const addToCart = () => { eventBus.publish('cart:add-item', { product, quantity: 1 }); }; return ( <div> <h3>{product.name}</h3> <button onClick={addToCart}>Add to Cart</button> </div> ); } // Usage in checkout micro-frontend function Cart() { const [items, setItems] = useState<CartItem[]>([]); useEffect(() => { return eventBus.subscribe('cart:add-item', (data) => { setItems(prev => [...prev, data as CartItem]); }); }, []); return <div>{/* render cart */}</div>; }
Routing Strategy
// Shell handles top-level routing function ShellApp() { return ( <BrowserRouter> <Layout> <Routes> <Route path="/catalog/*" element={<CatalogApp />} /> <Route path="/checkout/*" element={<CheckoutApp />} /> <Route path="/account/*" element={<AccountApp />} /> </Routes> </Layout> </BrowserRouter> ); } // Each micro-frontend handles its own sub-routes function CatalogApp() { return ( <Routes> <Route index element={<ProductList />} /> <Route path=":productId" element={<ProductDetail />} /> <Route path="category/:categoryId" element={<CategoryPage />} /> </Routes> ); }
Conclusion
Micro-frontends are a powerful pattern for scaling frontend development across large teams. The key decisions are:
- Integration approach: Module Federation for runtime composition
- Shared dependencies: Careful management to avoid bloat
- Communication: Event bus for loose coupling
- Routing: Shell owns top-level, micro-frontends own sub-routes
- Styling: Shared design system with scoped styles
Remember: micro-frontends add complexity. Only adopt them when the organizational benefits outweigh the technical costs.
Related Articles
Software Architecture16 min read
Microservices vs Monolith: A Practitioner's Decision Framework
Microservices vs monolith comparison with real decision criteria. Learn when microservices add value vs unnecessary complexity, with examples from production systems.
Frontend Engineering17 min read
React Performance Optimization at Scale
Optimize React performance with useMemo, useCallback, and code splitting. Real performance metrics and patterns from large-scale production applications.
Frontend Engineering15 min read
State Management Architecture in Modern React
Compare React state management solutions: Redux Toolkit, Zustand, and Context API. Learn when to use each with real examples and performance considerations.
Performance & SEO16 min read
Frontend Performance Budgets: Setting and Enforcing Limits
Set and enforce performance budgets with Webpack, Lighthouse CI, and bundle analyzers. Covers automated testing, CI/CD integration, and team accountability.
Performance & SEO19 min read
Core Web Vitals Optimization: A Technical Deep Dive
Improve Core Web Vitals scores with practical LCP, FID, and CLS optimization techniques. Real before/after metrics from production React and Next.js applications.