Frontend Engineering

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.

Khalid Aboubakr
18 min read
Micro FrontendsModule FederationArchitectureScalabilityTeamsSingle SpaWebpack

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:

  1. Integration approach: Module Federation for runtime composition
  2. Shared dependencies: Careful management to avoid bloat
  3. Communication: Event bus for loose coupling
  4. Routing: Shell owns top-level, micro-frontends own sub-routes
  5. 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

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 & 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.