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.