Micro-Frontend Architecture
Decompose monolithic frontends into independently deployable units. Covers composition patterns, module federation, shared state management, and the patterns that let multiple teams ship frontend features independently.
As organizations grow, the monolithic frontend becomes a bottleneck. Ten teams cannot merge changes to a single React app without stepping on each other. Micro-frontends apply the microservices principle to the frontend: each team owns a slice of the UI, builds it independently, deploys it independently, and the compositions come together at runtime.
Composition Patterns
Build-Time Composition:
Approach: NPM packages imported at build time
Pros: Simple bundling, type safety, tree shaking
Cons: Shared deployment, not independently deployable
Best for: Shared component libraries, design systems
// package.json
"@team-checkout/widget": "^2.0.0",
"@team-search/bar": "^1.5.0"
Server-Side Composition:
Approach: Server assembles HTML fragments from each micro-frontend
Pros: SEO-friendly, fast initial load
Cons: Complex server orchestration, not interactive until hydrated
Best for: Content sites, SSR applications
// Edge/server-side include
<esi:include src="/fragments/checkout-widget" />
<esi:include src="/fragments/search-bar" />
Client-Side Composition (Module Federation):
Approach: Browser loads modules from different servers at runtime
Pros: Truly independent deployment, runtime loading
Cons: Larger bundle, runtime errors possible
Best for: Large SPAs, multiple teams
// webpack.config.js (Module Federation)
new ModuleFederationPlugin({
name: "shell",
remotes: {
checkout: "checkout@https://checkout.cdn.com/entry.js",
search: "search@https://search.cdn.com/entry.js",
},
})
// Usage in shell app
const CheckoutWidget = React.lazy(
() => import("checkout/Widget")
);
Module Federation Example
// Team A: Shell Application (webpack.config.js)
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "shell",
remotes: {
productCatalog: "productCatalog@https://products.app.com/remoteEntry.js",
checkout: "checkout@https://checkout.app.com/remoteEntry.js",
userProfile: "userProfile@https://profile.app.com/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" },
},
}),
],
};
// Shell App component
function App() {
return (
<div>
<Header />
<Routes>
<Route path="/products/*" element={
<React.Suspense fallback={<Loading />}>
<ProductCatalog /> {/* From Team B */}
</React.Suspense>
} />
<Route path="/checkout/*" element={
<React.Suspense fallback={<Loading />}>
<CheckoutFlow /> {/* From Team C */}
</React.Suspense>
} />
</Routes>
<Footer />
</div>
);
}
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Shared mutable global state | One micro-frontend breaks another | Event-based communication, isolated state |
| Different React versions per micro-frontend | Massive bundle size, incompatibilities | Share React as singleton via Module Federation |
| No design system | Inconsistent UI across teams | Shared design system package, consumed by all |
| Micro-frontends for small teams | Overhead > benefit | Micro-frontends solve SCALE problems (5+ teams) |
| No contract testing between shell and remotes | Deployment breaks composition | Contract tests verify interface compatibility |
Micro-frontends are an organizational scaling pattern, not a technical improvement. A monolithic frontend built by one team is simpler and faster. Micro-frontends become necessary when team boundaries create deployment bottlenecks.