Micro-Frontends

Micro-Frontends

"An architectural style where independently deliverable frontend applications are composed into a greater whole." — Cam Jackson, martinfowler.com/articles/micro-frontends.html

Intent

Micro-frontends extend the microservices idea to the frontend layer: a single user-facing product is composed of multiple independently developed, independently deployed frontend applications. Each application is owned end-to-end by a single team — from database to UI — and can be released without coordinating with other teams.

Micro-frontends is the only architecture in this vault where the primary adoption criterion is organisational (team count and independent release cadences) rather than technical (codebase complexity, read/write ratio, scaling profile). Every other architecture in this vault is adopted because the codebase demands it. Micro-frontends are adopted because the organisation demands it.

TypeScript-only examples in this note — micro-frontends is a frontend architecture pattern and Java backend integration (e.g., Thymeleaf fragments) is a niche approach not worth vault space.

When NOT to Use

Micro-frontends are justified only when 3+ independent frontend teams ship to the same product with independent release cadences.

Additional criteria that disqualify the pattern:

  • Single frontend team — a single team gains deployment complexity (separate CI pipelines, Module Federation config, cross-micro-frontend contract versioning) with zero independence benefit. Use a modular frontend monolith instead, organised by feature (VSA applied to the frontend layer).
  • Shared release cadence — if all frontend teams deploy together on every sprint, the overhead of independent deployment adds no value. The independence benefit only materialises when teams genuinely ship on different schedules.
  • Prototype or MVP stage — premature decomposition kills velocity. Establish the product, understand the domain boundaries, and grow the team before splitting the frontend.

ThoughtWorks Technology Radar has tracked micro-frontend adoption reversals by teams that adopted without meeting the team-size threshold. The pattern moved to "Adopt" in May 2020, but the radar entry notes that over-application — particularly by single teams or teams with shared releases — has led organisations to revert to frontend monoliths.

The Complexity Cost Is Front-Loaded

Micro-frontends pay their cost immediately: separate CI/CD pipelines, shared dependency version negotiation, cross-micro-frontend communication contracts, integration test environments, and distributed CSS coordination. The independence benefit is only realised later, when teams genuinely ship at different cadences. Teams that adopt without meeting the threshold pay the cost and never collect the benefit.

When to Use

  • 3+ independent frontend teams each owning a distinct product domain
  • Teams shipping different user-facing domains — for example: catalog team (product listing and search), checkout team (cart, payment, order confirmation), account team (profile, order history, preferences)
  • Independent release cadences — the catalog team deploys Tuesday, the checkout team deploys Friday, without coordination
  • Organisations migrating a monolith frontend incrementally via Strangler-Fig-Pattern — new domains built as micro-frontends while legacy pages remain in the monolith
  • Products where different frontend areas have genuinely different technology choices (e.g., one team uses React, another uses Vue) and forcing convergence would delay delivery

How It Works

A shell application (also called host or container app) provides the frame: global routing, shared layout (header, navigation, footer), authentication context, and error boundaries. The shell itself contains no domain logic — it composes the product from independently deployed micro-frontends.

Each micro-frontend application is an independently deployable unit. It owns a bounded slice of the user-facing product (a domain), its own build pipeline, its own deployment, and its own backend integration. Teams treat other micro-frontends as external dependencies, not internal modules.

Composition Strategy Comparison

StrategyHow it worksTradeoffsBest for
Build-time integrationMicro-frontends published as npm packages; host imports and bundles them at build timeSimplest to implement; couples release cadences — all teams must rebuild and redeploy together when any package changesSmall teams where release coupling is acceptable; shared component libraries
Server-side compositionServer assembles HTML fragments from multiple micro-frontend services before sending to client (Server-Side Includes, Next.js zones, Nginx SSI)Zero client-side composition overhead; strong SSR performance; requires server infrastructure and CDN configurationSSR-first architectures; content-heavy sites with SEO requirements
Runtime via Module FederationEach micro-frontend deployed independently; host fetches remote modules at runtime via remoteEntry.js; shared dependencies deduplicated via shared configTrue deployment independence; added build config complexity; shared dependency version negotiation requiredThe modern default for teams with genuine independent deployment needs
Edge-side composition (ESI/Edge Workers)CDN or edge worker assembles page from micro-frontend fragments at the edge using ESI tags or Cloudflare WorkersMinimal client JS, CDN-level caching, low latency; highest infrastructure complexity; limited debugging toolingContent-heavy sites, personalisation at scale, global CDN deployments

Runtime composition via Module Federation is the dominant modern approach and receives the most depth in the TypeScript Example section below. The other three strategies are recognised in the table — for new projects with genuine independent deployment needs, Module Federation is the correct starting point.

The CSS Isolation Problem

CSS from one micro-frontend bleeds into others unless actively isolated. Three approaches: Shadow DOM (full isolation, limited styling flexibility), CSS Modules (build-time class scoping, framework-dependent — the pragmatic default), BEM naming conventions (manual discipline, no enforcement). Test composed views in integration — styles that look correct in isolation break when multiple micro-frontends share a single page.

The Shared Design System Challenge

Every micro-frontend team needs a shared component library for visual consistency, but the library itself becomes a coordination bottleneck. Mitigation: version the design system as a package, allow teams to pin versions independently, run visual regression tests.

Architecture Diagram

Micro-Frontends-diagram.excalidraw

TypeScript Example

Webpack Module Federation — Host + Remote

Module Federation is built into Webpack 5 via webpack.container.ModuleFederationPlugin — no additional package required. The pattern: a remote app exposes a module via exposes and publishes a remoteEntry.js manifest; a host app references the remote in remotes and lazy-loads exposed modules at runtime.

Remote (catalog micro-frontend):

// catalog/webpack.config.ts
// Source: webpack.js.org/plugins/module-federation-plugin/
import webpack from 'webpack';
const { ModuleFederationPlugin } = webpack.container;
 
export default {
  output: { uniqueName: 'catalogApp' },
  plugins: [
    new ModuleFederationPlugin({
      name: 'catalog',
      filename: 'remoteEntry.js',               // fetched by the host at runtime
      exposes: {
        './ProductList': './src/ProductList',    // exposes this component to consumers
      },
      shared: {
        react:     { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

Host (shell application):

// shell/webpack.config.ts
import webpack from 'webpack';
const { ModuleFederationPlugin } = webpack.container;
 
export default {
  output: { uniqueName: 'shellApp' },
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        catalog: 'catalog@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react:     { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};

Consuming in the host application:

// shell/src/App.tsx — lazy-load the remote component
import React, { Suspense, lazy } from 'react';
 
// Module Federation resolves 'catalog/ProductList' to the remote at runtime
const ProductList = lazy(() => import('catalog/ProductList'));
 
export function App() {
  return (
    <Suspense fallback="Loading catalog...">
      <ProductList />
    </Suspense>
  );
}

The singleton: true flag on react and react-dom is mandatory. Without it, each micro-frontend and the host may load separate React instances — causing Invalid hook call errors at runtime because hooks must be called from the same React instance they were registered with.

Vite users

@originjs/vite-plugin-federation provides equivalent exposes / remotes / shared configuration for Vite and Rollup builds. Install with npm install --save-dev @originjs/vite-plugin-federation — the API mirrors the Webpack plugin with minor syntax differences.

PatternRelationship
Modular-MonolithBackend analogue — modules with enforced boundaries within a single deployment unit. When a frontend team is small or shares a release cadence, a modular frontend monolith (VSA applied to the frontend) is the correct architecture. The choice between Modular Monolith and Micro-frontends on the frontend mirrors the same choice on the backend.
Strangler-Fig-PatternIncremental migration path from a monolith frontend. New domains are built as micro-frontends while legacy pages remain in the monolith; the legacy shrinks over successive releases. Migration is complete when the legacy is deleted, not merely bypassed.
Vertical-Slice-ArchitectureFeature-oriented decomposition at the code level vs team-level decomposition in micro-frontends. VSA is used within a single codebase (or within a single micro-frontend) to organise features. A team below the micro-frontends threshold should apply VSA to their frontend monolith instead of adopting micro-frontends.
Bounded-ContextTeam boundaries align with micro-frontend boundaries. Each micro-frontend corresponds to a bounded context — the catalog micro-frontend owns the catalog domain, the checkout micro-frontend owns the checkout domain. This alignment is the organisational prerequisite that justifies the architectural complexity.
Facade-PatternThe shell/host application acts as a Facade over the composed micro-frontends — it presents a unified product surface to the user while hiding the compositional complexity underneath. The shell's public contract (routes, auth context, shared layout) is the facade interface.
Observer-PatternCross-micro-frontend event communication uses the browser's CustomEvent API or a typed event bus — effectively the Observer pattern. This is the correct mechanism for micro-frontend coordination; direct module imports across micro-frontends re-introduce build coupling.
Anti-Corruption-Layer-PatternWhen the shell needs to pass data to micro-frontends, a typed contract (props, URL parameters, or a typed event bus) acts as an ACL — preventing the host's domain model from leaking into remote micro-frontends.

Common Pitfalls

Pitfall 1: Adopting Without Team-Size Justification

A single frontend team adopts micro-frontends to "enable future scale." They gain complexity immediately — separate CI pipelines, deploy coordination, Module Federation config — with none of the independence benefit, because the same team ships all micro-frontends together anyway.

Warning signs: A team referring to "our micro-frontends" but acknowledging they all deploy together every sprint.

Pitfall 2: Missing singleton: true on Shared React

Without singleton shared config, the host and each remote may load separate React instances. React hooks throw Invalid hook call errors because hooks must be called from the same React instance they were registered with. Both host and every remote must declare react and react-dom with singleton: true.

Warning signs: Invalid hook call error in the browser console when rendering a remote component; react appearing twice in the webpack bundle analyser.

Pitfall 3: CSS Bleed in Composed Views

Each micro-frontend looks visually correct in its isolated dev server. Global CSS resets or component styles from one micro-frontend override another's styles at composition time. Collisions only appear when multiple micro-frontends are loaded on the same page.

Warning signs: Styles look correct in isolation but break in the host shell application.

Pitfall 4: Distributed Monolith via Shared Global State

Multiple micro-frontends share a global state store (e.g., a single Redux store in the host). Any micro-frontend can write to any part of the state, recreating tight coupling at the runtime layer.

Correct approach: Each micro-frontend owns its own state. Cross-micro-frontend communication via typed custom browser events, URL parameters, or a minimal typed event bus — never via import { store } from 'shell/globalStore'.

Sources

  • martinfowler.com/articles/micro-frontends.html — Cam Jackson canonical article; canonical definition; composition strategy taxonomy; team organisation principles
  • webpack.js.org/concepts/module-federation — Runtime composition conceptual overview; host/remote config pattern; shared deduplication behaviour
  • webpack.js.org/plugins/module-federation-plugin/ — Full ModuleFederationPlugin option reference; exposes, remotes, shared, singleton, requiredVersion fields
  • thoughtworks.com/radar/techniques/micro-frontends — ThoughtWorks Technology Radar entry; "Adopt" ring as of May 2020; adoption reversal observations
  • npmjs.com/@originjs/vite-plugin-federation — v1.4.1 (verified March 2026); Vite Module Federation equivalent

Notes that link here: Design-Patterns-MOC, Application-Architecture-MOC, Facade-Pattern, API-Gateway-Pattern, Feature-Flags-Pattern, State-Management-Patterns (each micro-frontend owns its own state; cross-MFE communication via typed events, not shared store), Frontend-Architecture-MOC