Micro-frontends exist because frontend monoliths become slow to develop when multiple teams work on the same codebase. But the solution introduces its own complexity, and most teams that adopt micro-frontends underestimate that complexity.
The problem micro-frontends solve
On large teams, 5+ squads working on the same frontend repository face: constant merge conflicts, builds that get slower each sprint, difficulty deploying independently per squad, and coupling between features that should be independent.
Micro-frontends split the application into independent units composed at runtime. Each squad controls their part, deploys independently, and doesn't block the others.
Available approaches
Build-time integration (Module Federation)
Webpack Module Federation lets applications load modules from other applications at runtime. Each micro-frontend is a standalone application that exports components.
// webpack.config.js: host
new ModuleFederationPlugin({
name: 'shell',
remotes: {
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
},
})
Runtime integration (iframe or Web Components)
iframes are the most isolated approach. Web Components allow more granular composition but require a communication layer between micro-frontends.
Server-side composition (SSI, ESI, edge-side includes)
The server composes micro-frontends before sending to the client. Eliminates loading waterfalls but requires server infrastructure that supports includes.
What changes in practice
// Typical structure
shell-app/ # Shell that assembles the page
├── src/
│ ├── Header.tsx # Shared across all
│ ├── Footer.tsx # Shared across all
│ └── MicroApp.tsx # Micro-frontend loader
│
checkout/ # Checkout squad
├── src/
│ ├── Checkout.tsx
│ ├── Payment.tsx
│ └── Shipping.tsx
│
catalog/ # Catalog squad
├── src/
│ ├── ProductList.tsx
│ └── ProductDetail.tsx
The costs nobody warns you about
- Communication between micro-frontends: shared data requires an explicit mechanism, event bus, shared store, or prop drilling via shell.
- Consistent styling: a shared design system is mandatory, not optional. Each squad can customize, but the base comes from a common package.
- Performance: multiple bundles mean more HTTP requests, more JavaScript parsed, more memory. The shell needs to optimize loading.
- Debugging: a bug that crosses boundaries between micro-frontends is harder to trace than in a monolith.
- Orchestrated deploy: dependencies between micro-frontends may require coordinated deploys, which eliminates part of the independence.
When it's worth it
Micro-frontends justify themselves when: you have 3+ teams working on the same frontend, with independent releases and little code overlap. The team velocity gain outweighs the infrastructure cost.
When it's not worth it
If you have 1-2 teams, or squads work on features that constantly overlap, a well-organized monolith with good folder and module structure is simpler and more efficient. Don't adopt architecture to anticipate problems you don't have.
Enjoyed this content?
I build web products and AI solutions the right way — solid architecture, maintainable code, and real delivery.
Let's talk