Five squads in the same frontend repository produce daily merge conflicts, a build that grows every sprint, and a release blocked by a team that never touches your feature. Micro-frontends split the application into units each squad ships alone. The trade shows up after adoption: runtime, deploys, and debugging gain new parts.

The problem the architecture attacks

Micro-frontends pay off under three conditions at once: three or more teams on the same frontend, independent releases per squad, and little code overlap between them. Cam Jackson, in the article Micro Frontends (martinfowler.com, 2019), frames the goal as increasing the effectiveness of teams working on the same product, and the cost shows up in keeping pieces coherent while they ship at different speeds.

A two-team squad with a clear domain boundary pays the micro-frontend infrastructure and gets no velocity back. Measure the time between opening a PR and reaching production before you commit to any migration.

Three ways to compose the page

Build-time integration: Module Federation

Webpack's Module Federation loads modules from another application at runtime. Each micro-frontend is a standalone application that exports a remote entry, and the shell decides which one loads per route. Rspack and the independent Module Federation project keep the same concept.

// webpack.config.js: shell
new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
    catalog: 'catalog@https://catalog.example.com/remoteEntry.js',
  },
})

Runtime integration with iframes

Isolation through iframes covers styles, JavaScript, and lifecycle completely. Communication moves to postMessage, and the cost shows up in accessibility, SEO, and session sharing across the pieces.

Server-side composition: SSI and ESI

The server assembles the page before sending it, and the client receives finished HTML. Orchestration leaves the browser, and the requirement becomes include infrastructure plus a clear HTML contract between teams.

Repository layout in practice

shell-app/              # Assembles the page and the base layout
├── src/
│   ├── Header.tsx      # Shared across all teams
│   ├── Footer.tsx      # Shared across all teams
│   └── MicroApp.tsx    # Micro-frontend loader
│
checkout/               # Checkout squad
├── src/
│   ├── Checkout.tsx
│   ├── Payment.tsx
│   └── Shipping.tsx
│
catalog/                # Catalog squad
├── src/
│   ├── ProductList.tsx
│   └── ProductDetail.tsx

Costs that show up in operations

  • Communication: shared data needs an event bus, a store in the shell, or a props contract. Without one, each squad keeps its own copy of the cart.
  • Styling: a versioned design system package is a prerequisite. Tokens and base components come from one place.
  • Performance: every extra bundle costs HTTP requests and JS parsing. The shell owns preloading and per-route lazy loading.
  • Debugging: a stack trace that crosses two bundles pulls two teams into the same call.
  • Deploys: dependencies between squads bring back the coordinated deploy the architecture was supposed to remove.

Teams that reach the fifth cost find that deploy independence survives only while the contracts between the pieces stay stable.

When to adopt

Adopt when three or more squads ship on the same product with no release coordination, the merge bottleneck already shows up in a metric, and each team can explain the domain boundary of its own slice. Team velocity gains room; the infrastructure cost joins the budget either way.

When to stay with the monolith

Two teams or fewer, overlapping features, and a shared release call for a modular monolith with folders and modules per domain. A new architecture does not repair a broken team process, and a well-partitioned monolith ships just as fast.

References