Frontend Handbook
Written

Design micro frontends

The method applied to a store built by four teams that release separately: when micro frontends are worth it, how the page is composed, shared dependencies, routing and event contracts, releases and failure isolation, and style isolation, with live demos.

"Our store is built by four teams: a platform team and the catalog, checkout and account teams. Every release waits for all of them. Design a frontend that lets each team ship on its own." Micro frontends are the usual answer: split the frontend into pieces that are built, tested and released separately, then put them back together so the shopper sees one store. The split mostly solves an organisational problem, and you pay for it in runtime integration, page weight and coordination. A strong answer says when that trade is worth making, then works through the design method as usual.

Every demo below works on the same sample store: a shell and three micro frontends, with a product page that shows pieces from three teams at once. The composition approaches and tools are compared side by side and none of them is taught on its own.

01

Requirements and architecture

Start with who needs what. Here the shoppers need one fast, consistent store and the engineers need to release without waiting for each other.

  • Functional: one store that looks and behaves like one product; each team owns its pages and widgets end to end, from UI to the API it calls; the header and the cart count are shared across every page.
  • Non-functional: a team releases and rolls back without the others; a broken piece doesn't take the page down; the product page stays inside its load-time and JavaScript budgets; the pieces look consistent; content is in the HTML for search engines.
  • Scope cuts: how many teams (four here), whether pieces from different teams share one page (yes, on the product page), whether teams may pick their own framework (no), and whether a legacy app has to keep running alongside (not here).
  • Architecture: a thin shell owned by the platform team handles routing, the header and the layout. Each team's micro frontend has its own repository or folder and pipeline and is deployed to its own versioned URL, and the shell keeps a manifest of which versions are live. Name the alternatives: one app, or a modular monolith that keeps one deploy.

Try it: the sample store and its alternatives

Sample store: a shell and three micro frontendsThe platform team's shell owns routing, the header and the layout, and composes three micro frontends: Catalog (Discovery team, home, search and product pages), Checkout (Checkout team, cart and checkout) and Account (Accounts team, account pages). Each has its own repository or folder and its own pipeline that builds, tests and deploys it to its own versioned URL. All of them use a shared design system and agreed event contracts, and the shell keeps a manifest of which version of each piece is live.ShellPlatform team · routing, header, layoutmanifest of live versionsCatalogDiscovery team/ /search /productsown repo or folderbuild · test · deployv3.8.1CheckoutCheckout team/cart /checkoutown repo or folderbuild · test · deployv2.4.0AccountAccounts team/accountown repo or folderbuild · test · deployv1.9.3CDN: each piece's files at its own versioned URLthe shell reads the manifest to know which version to loadShared by every piece, owned by the platform teamdesign system and theme tokens · event and URL contracts · shared libraries (framework, router)
One example. Arrows point from the shell to the pieces it composes, at build time, on the server or in the browser (topic 3 compares them). The shell is thin: it owns the frame and routing and holds no product features. A smaller store could start with only the route-level split (catalog and checkout as separate apps under one domain) and never compose pieces on the same page.
  • One app

    One codebase, one build, one deploy. Teams work in folders of the same app.

    Wins
    Simplest to build, test and run; one copy of every library; refactors across features are one commit.
    Costs
    Every team ships on the same release train, and one team's broken build or test blocks the rest.
  • Modular monolith

    One deployed app made of packages with enforced boundaries, often in a monorepo.

    Wins
    Clear ownership and boundaries without runtime integration; affected builds and caching keep CI fast.
    Costs
    Still one deploy: teams can work apart but can't release apart, and everyone upgrades the framework together.
  • Micro frontends

    Separately built and released pieces, put together into one product at build time, on the server or in the browser.

    Wins
    Each team releases on its own schedule and can roll back alone; a legacy app can be replaced one route at a time.
    Costs
    Integration work at runtime, shared-library negotiation, cross-team contracts, consistent styling and more to monitor.
  • Do: Split by business domain, so each team owns a slice of the product (catalog, checkout) with its pages, widgets and API.

    Avoid: Splitting by technical layer (a header team, a components team, a data team): every feature then needs all of them.

Read more:Cam Jackson: Micro Frontends(opens in a new tab)micro-frontends.org(opens in a new tab)The design method(opens in a new tab)

02

Should you use micro frontends?

Micro frontends fix one problem well: teams that block each other's releases. If that isn't your problem, they add cost and solve nothing. A senior answer argues from criteria, and the first question is how many teams there are.

  • One team has nobody to release independently from. Keep one app with clear feature folders.
  • Several teams that rarely collide do well with a modular monolith: packages with enforced boundaries and owners in one deploy, as in the monorepo case study.
  • When releases block each other every week, or a legacy app must be replaced one route at a time, separate releases start paying for themselves. Then pick the lightest composition that fits: a route-level split when each team owns whole pages, and composition on one page only when pieces have to share a page.
  • Count the cost before you commit: a platform team to own the shell and shared libraries, contracts between teams, more to monitor, and every extra framework loaded by every visitor.

Try it: describe your situation and get a recommendation

How many teams build the frontend?
How often does one team's release wait on another's?
Frameworks
How do the teams' parts meet?
Performance budget

Recommendation

Micro frontends, composed with: edge composition

  • Releases block each other often, and independent deploys remove the shared release train.
  • With many teams, one deploy pipeline becomes the bottleneck and the blast radius of every bad merge.
  • Pieces share a page and search engines and first paint need the content in the HTML, so compose on the server or at the edge.

One example rule set. Real decisions also weigh how much platform work you can staff: someone has to own the shell, the shared libraries and the contracts. The route-level split is the cheapest form, so check whether it is enough before composing pieces on one page.

  • Do: Write down the release conflict you are solving, and try a route-level split before you compose pieces on one page.

    Avoid: Adopting micro frontends so each team can choose its own framework, or because the architecture looks modern.

Read more:Next.js: Multi-Zones(opens in a new tab)Vercel: Microfrontends(opens in a new tab)Building Micro-Frontends, 2nd ed.(opens in a new tab)

03

Composition: where the page is put together

Composition is where separately built pieces become one page, and it is the decision every later one depends on. You can compose at the router, at build time, on the server, at the edge, in the browser, or with iframes.

  • A route-level split gives each team whole pages and never mixes them. It is the simplest form, and moving between teams' pages becomes a full page load.
  • Build-time packages are easy, but the shell has to rebuild to ship a team's change. That brings back the shared release train you were trying to remove.
  • Server and edge composition put every piece in the first HTML, which is good for search and first paint, but someone has to run the layer that stitches the fragments. On the server the page waits for the slowest fragment; at the edge, cached pieces arrive first and personalised ones follow.
  • Client-side runtime composition (Module Federation, import maps) needs only static files and shares libraries well. The cost is a chain of requests before content appears. Iframes isolate everything, and you lose shared libraries, simple routing and easy accessibility.

Try it: compose the product page six ways

Compose the product page with

The browser loads the shell, reads a manifest, then loads each piece's code from its own URL at runtime and shares libraries between them.

Product details on screen after 1040 ms; every piece interactive after 1040 ms; 300 kB of JavaScript.

Each step of loading the product page with Client-side runtime, with its start and end time
StepTimeline (0 to 1040 ms, the same scale for every approach)
Static shell HTML from the CDN (network)
from 0 ms to 100 ms
Shell JS and shared framework (90 kB) (script)
from 100 ms to 380 ms
Manifest of live versions (network)
from 380 ms to 480 ms
Catalog entry file (network)
from 480 ms to 580 ms
Catalog code (90 kB) (script)
from 580 ms to 860 ms
Catalog data (data)
from 860 ms to 1010 ms
Catalog renders (render)
from 1010 ms to 1040 ms
Checkout entry file (network)
from 480 ms to 580 ms
Checkout code (70 kB) (script)
from 580 ms to 820 ms
Checkout data (data)
from 820 ms to 970 ms
Checkout renders (render)
from 970 ms to 1000 ms
Account entry file (network)
from 480 ms to 580 ms
Account code (50 kB) (script)
from 580 ms to 780 ms
Account data (data)
from 780 ms to 930 ms
Account renders (render)
from 930 ms to 960 ms

A model, not a measurement. The thin vertical line marks when product details appear. Assumed: 100 ms round trip to the origin, 30 ms to the CDN edge, 2 ms per kB of JavaScript to download and parse, 150 ms per data request from the browser, 30 ms to render or hydrate a piece, and a 50 kB shared framework. Preloading, HTTP caching and streaming would shorten every line.

Teams release independentlyYes
Update the manifest and the next page load picks up the new version.
No composition server to runYes
Static files on a CDN.
Content in the first HTMLNo
Content appears after several round trips, unless server rendering is added on top.
Shared libraries load onceYes
The runtime negotiates one copy of shared libraries.
Styles and scripts isolatedPartly
Possible with shadow DOM or scoped CSS, not by default.
Teams can mix frameworksPartly
Possible, but every framework is another download.
Common tools for each approach, as of October 2026. Rows for the picked approach are highlighted.
ToolApproachStatusWhat it does
Next.js Multi-ZonesRoute-level splitDocumented in Next.js 16Separate Next.js apps per set of paths, each with its own assetPrefix, joined by rewrites. Moving between zones is a full page load, so links across zones use <a>.
Vercel MicrofrontendsRoute-level splitGenerally availableManaged path routing of several projects under one domain, set in microfrontends.json.
A reverse proxy or CDN rulesRoute-level splitAny hostPath-based routing in nginx or your CDN does the same job without a framework feature.
Workspace packagesBuild-time packagesAny package managerEach piece is a package in a monorepo or on a registry; the shell installs and bundles them.
Server Side Includes (nginx)Server-sideStableAn include directive in the HTML names a fragment URL; nginx fetches several includes in parallel.
PodiumServer-sideMaintained (5.4.10)Node.js podlets serve HTML fragments; a layout server fetches and stitches them.
Tailor (Zalando)Server-sideArchived (2022)A streaming layout service for fragments. Its repository is archived, so pick something maintained.
Edge Side IncludesEdgeW3C Note from 2001<esi:include src="…" alt="…"> with a fallback URL, processed by CDNs that support it.
web-fragments (Cloudflare)EdgePre-1.0 (0.8.2)Renders fragments on edge workers and moves them into a client app once ready.
Module Federation 2.0Client-side runtimeActive (2.9.2)Shared-library negotiation, a manifest, runtime remotes and plugins. Runs on webpack, Rspack and Vite (@module-federation/vite).
Native FederationClient-side runtimeActive (core 4.8.1)The Module Federation model on ES modules and import maps, with an Angular adapter that supports SSR.
single-spa + import mapsClient-side runtime6.0.3 stable, 7.0 in betaA root config mounts apps by route; its docs recommend import maps to load each app and the framework once.
Module Federation for Next.jsClient-side runtimeMaintenance modePages Router only, supports up to Next.js 15. Next.js points to Multi-Zones instead.
Plain iframes + postMessageIframesBuilt into browsersNo library needed; each frame needs a title, and its height has to be sent to the parent.

One snapshot. These tools change quickly, so check their docs before you choose.

  • Do: Pick per page: a route-level split by default, server or edge composition where pieces share a page and SEO matters, runtime composition where they don't.

    Avoid: A client-side waterfall on a page that needs fast first paint, with no preloading of the manifest and entry files.

Read more:Module Federation docs(opens in a new tab)single-spa: recommended setup(opens in a new tab)nginx: SSI module(opens in a new tab)Podium(opens in a new tab)

04

Deep dive: shared dependencies

Four teams that each bundle the framework would make every shopper download it four times. Runtime composition avoids that with a share scope: every piece registers the versions it brings, and the runtime decides which copy each piece uses.

  • Module Federation picks the highest version in the scope that fits each piece's accepted range, which defaults to the range in its package.json. A piece that finds nothing compatible falls back to its own copy, so two versions end up on one page.
  • Libraries that keep state, such as React, must be singleton: one copy for everyone. A piece whose range the singleton doesn't fit gets a console warning and runs on it anyway, or fails to load with strictVersion. A copy that has already loaded is never swapped for a higher version that registers later, so load order matters.
  • Share subpaths too. Sharing react-dom without a trailing slash leaves react-dom/client bundled locally, which quietly loads a second React DOM.
  • The design system is the other big shared dependency. Sharing more makes pages smaller and couples upgrades; sharing less frees each team and grows the page. A common setup shares the framework, router and design system as singletons and lets each piece bundle everything else. Roll out a design system major the way you would an API change: support the old and new versions for a while.

Try it: which copy of a shared library does each piece get?

Shared library
Share strategy
For each piece, the version it bundles, the range it accepts and the version it ends up using
PieceBundlesAcceptsResult
Shellhost, loads first19.2.0 · sharedUses the single shared 19.2.0.
Catalog19.2.0 · sharedUses the single shared 19.2.0.
Checkout19.2.0 · sharedUses the single shared 19.2.0.
Account19.2.0 · warningConsole warning: Unsatisfied version 19.2.0 of shared singleton module react (required ^18.2.0). Runs on 19.2.0 anyway and may break.

1 copy of react + react-dom on the page

Simplified from webpack 5 Module Federation's runtime, which MF 2.0 follows for these cases. Sizes are assumptions. Under "loaded first" the shell's copy is already in use when the others register, so it stays the singleton even when a higher version arrives. With plain import maps there is nothing to negotiate: the map names one URL per library, and every piece that imports it gets that copy.

  • Do: Make the framework and design system singletons, align their major versions across teams on a schedule, and set a size budget per piece.

    Avoid: Leaving sharing to defaults, then finding two copies of React in production through an "Invalid hook call" error.

Read more:Module Federation: shared(opens in a new tab)Module Federation: shareStrategy(opens in a new tab)webpack: ModuleFederationPlugin(opens in a new tab)

05

Deep dive: routing and communication

How pieces find each other and talk decides how coupled the teams really are. Keep the shared surface small and treat every part of it as a versioned contract.

  • Routing is two-level. The shell maps top-level prefixes to pieces (/products to catalog, /cart to checkout) and each piece routes inside its prefix. Adding a section means one change to the shell's table; everything else stays inside a team.
  • Prefer the URL for state that should survive a reload, and browser events for news between pieces (an item was added). Give events a team prefix, and have a piece read current state when it mounts, since it can't receive events fired before it loaded.
  • Avoid a global store shared by every piece. It ties all teams to one state shape and one library version, and changing it becomes a coordinated release.
  • Event payloads, props passed from the shell and URL parameters are APIs. Adding a field is safe. Renaming, removing or changing a field's type breaks every consumer still on the old shape, at runtime and with no compile error.

Try it: route a URL, then change an event's shape

Who owns this URL?

The shell matches /products and mounts Catalog (Discovery team), whose own router handles /espresso-grinder. The header and layout stay the shell's.

Catalog adds an item to the cart

Catalog's next release changes the event

dispatchEvent(new CustomEvent("cart:item-added", { detail: {"sku":"GRN-200","quantity":1,"price":89} }))

Compatible change Every field from version 1 is still sent with the same type, so listeners written against it keep working.

  • Header cart count (Shell)3 items: reads only quantity.
  • Mini cart total (Checkout)€209: every field it reads is there with the type it expects.

Before the event the cart held 2 items worth €120. Both listeners were written against version 1: { sku: string, quantity: number, price: number }. One example contract; teams often publish it as a typed package or a JSON Schema and run a contract test in each consumer's pipeline.

  • The URL

    Anything that should survive a reload or a shared link: the product id, search terms, filters.

    Cost: Only strings, and only for state that belongs in the address.

  • Browser events

    Fire-and-forget news between pieces (an item was added, the user signed in), with a team prefix in the event name.

    Cost: No delivery guarantee: a piece that mounts later misses earlier events, so it reads current state on mount.

  • A shared store

    Rarely. One global store couples every team to one state shape and one library version.

    Cost: Changing the store is a coordinated release, which is what micro frontends were meant to avoid.

  • Do: Publish each contract as a typed package or schema, make only additive changes, and run consumer contract tests in every team's pipeline.

    Avoid: Pieces reaching into each other's DOM, globals or storage keys, or a shared store that every team writes to.

Read more:MDN: CustomEvent(opens in a new tab)Cam Jackson: Micro Frontends(opens in a new tab)micro-frontends.org(opens in a new tab)

06

Deep dive: releases and failure isolation

Independent release is the reason to adopt micro frontends, and containing failure is what makes it safe. Each piece should be able to go out, go wrong and come back without touching the others.

  • Deploy each build to its own immutable, versioned URL and let the shell read a manifest of live versions. Releasing changes one manifest entry, and rolling back points it at the previous build, which is still on the CDN.
  • Wrap every piece in an error boundary with a fallback, and give it a timeout, so a crash or a slow server costs one slot instead of the page. Keep the shell small and boring: nothing above it can catch its errors.
  • Boundaries don't catch a broken contract, because the page renders with wrong data. That needs contract tests before release and monitoring of errors and key numbers per piece after it.
  • Roll out gradually: send a share of users to the new version through the manifest and watch that piece's errors before going to everyone. The catch is that two versions run at once, so contracts must work with both.

Try it: release, break and roll back one piece

  • Shell (Platform team)v4.2.0

  • Catalog (Discovery team)v3.8.1

  • Checkout (Checkout team)v2.4.0

  • Account (Accounts team)v1.9.3

manifest.json {"shell":"4.2.0","catalog":"3.8.1","checkout":"2.4.0","account":"1.9.3"}

What the shopper sees on /products/espresso-grinder

ShellHeader, nav and cart count
CatalogProduct details
CheckoutMini cart
AccountRecently viewed

Every piece rendered.

  • One broken piece doesn't take down the page
  • Page meets its load-time budget
  • Pieces agree on shared data
  • Each team released without waiting for the others

Simplified. Each release bumps a minor version and only that piece's entry in the manifest changes; rolling back points the manifest at the previous build, which is still on the CDN. Without isolation the shell renders every piece in one tree and waits for all of them. With it, an error boundary catches a crash and a 1.5 s timeout lets the rest of the page render first.

  • Do: Give every slot a boundary, a timeout and a fallback, tag errors and metrics with the piece and version, and keep old builds on the CDN.

    Avoid: Deploying all pieces together "to be safe": that is a distributed monolith with all of the cost and none of the independence.

Read more:React: error boundaries(opens in a new tab)Module Federation: runtime plugins(opens in a new tab)Module Federation: manifest(opens in a new tab)

07

Deep dive: styling and consistency

Style collisions are easy to ship when teams release separately. Two teams write .title, the one that loads last wins, and the header changes when checkout deploys.

  • A team prefix on every class (checkout-title) is cheap and works if everyone follows it. Scoped class names, generated by CSS Modules and similar tools, do the same automatically. Neither stops element selectors such as button from leaking.
  • A shadow tree blocks selectors in both directions. Inherited properties (font, color) and CSS custom properties still cross the boundary, so theme tokens reach every piece. The cost is that global styles, including the design system's, don't; each shadow root has to load them itself.
  • Consistency comes from one design system with shared tokens for color, spacing and type, owned by the platform team and loaded once. It should hold UI only, never business logic.

Try it: make checkout's styles leak, then stop them

Isolate checkout's styles with
The shell (header) and checkout's mini cart, rendered with the CSS on the right.
/* shell */
.title { font-weight: 600; color: var(--brand); }
.button { background: var(--primary); border-radius: 6px; padding: 4px 12px; … }

/* checkout */
.title { text-transform: uppercase; letter-spacing: 0.06em; }
.button { background: var(--destructive); color: var(--background); }
.price { color: var(--brand); font-weight: 600; }

    Real CSS, read back with getComputedStyle. Scoped names are what CSS Modules and similar tools generate at build time; the prefix is the same idea done by hand. The theme tokens are this site's own custom properties, which is why the demo follows light and dark mode even inside the shadow roots.

    • Do: Use scoped class names by default, theme through shared custom properties, and keep global CSS to a reset and the design system.

      Avoid: Element or generic class selectors in a piece's CSS, and a design system copied into each team's repository.

    Read more:MDN: using shadow DOM(opens in a new tab)MDN: ::part()(opens in a new tab)CSS Modules(opens in a new tab)

    Back to the method: this case study used the same stages as the design method and the other case studies: the news feed, the autocomplete and the monorepo. The monorepo and micro frontends answer different questions and often go together: one is how teams share code, the other is how they release it.