When micro-frontends are worth it
"When are micro-frontends worth it, and when are they not?"
What it is
Micro-frontends split a single web application into independently built and deployed pieces, each owned by a different team, composed at build time, server time or run time into one user experience.
The three composition strategies, which are the actual decision:
| Strategy | How | Deploy independence | Runtime cost |
|---|---|---|---|
| Build-time (npm packages) | Each team publishes a package; the shell builds them in | No. Shell must rebuild and redeploy | None |
| Server-side (SSI, ESI, fragments) | Fragments assembled by the server or CDN | Yes | One extra fetch per fragment |
| Run-time (Module Federation, iframes, web components) | Shell loads remotes in the browser | Yes | Duplicate dependencies, extra requests |
Commonly confused with "splitting the codebase". A monorepo with clear module boundaries gives you code organisation. Micro-frontends give you independent deployment, and that is the only benefit that justifies the cost. If teams still coordinate releases, you have paid the price and bought nothing.
Also commonly confused with microservices on the frontend. The analogy misleads, because backend services are isolated by process and network while frontend fragments share one DOM, one global scope, one CSS cascade, one router and one memory space. The isolation is far weaker, which is where most of the difficulty comes from.
The problem it solves
One problem, precisely: a single deployment pipeline shared by many teams becomes a coordination bottleneck.
The symptoms, which are what you look for before recommending this:
- A release train where one team's regression blocks everyone's release.
- Merge queue times measured in hours, and a build measured in tens of minutes.
- Cross-team coordination meetings whose only purpose is sequencing deploys.
- A team that cannot ship a one-line copy change without waiting for the next window.
These are real costs and they scale with team count. With three teams they are irritating. With fifteen they can dominate delivery.
What micro-frontends do not solve: a slow build (that is a build problem), tangled code (that is a module boundary problem), or a shared design system (that gets harder, not easier). A team proposing micro-frontends to fix any of those has misdiagnosed.
Mechanics
Module Federation, the dominant runtime approach
// shell/webpack.config.js -- the container application
new ModuleFederationPlugin({
name: 'shell',
remotes: {
// Loaded at runtime from a URL, so the checkout team deploys
// without the shell rebuilding.
checkout: 'checkout@https://checkout.example.com/remoteEntry.js',
search: 'search@https://search.example.com/remoteEntry.js',
},
shared: {
// singleton: true means one React instance across all remotes.
// Without it, hooks break in confusing ways, because two React
// copies do not share the internal dispatcher.
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
})
// checkout/webpack.config.js -- the remote
new ModuleFederationPlugin({
name: 'checkout',
filename: 'remoteEntry.js',
exposes: { './Cart': './src/Cart' },
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
},
})
// The shell consumes it lazily, with a boundary, because a remote
// that fails to load must not take down the page.
const Cart = React.lazy(() => import('checkout/Cart'));
<ErrorBoundary fallback={<CartUnavailable />}>
<Suspense fallback={<CartSkeleton />}>
<Cart />
</Suspense>
</ErrorBoundary>
That error boundary is not optional decoration. A runtime-composed application takes a dependency on another team's CDN being available during your user's session, which is a availability property no build-time composition has.
The four problems that are genuinely hard
1. Shared dependency versions. singleton: true means one React across all
remotes, which means every team upgrades React together, which is exactly the
coordination you were trying to remove. Drop the singleton and you ship two Reacts (a
40 KB penalty each) and hooks break across the boundary. There is no clean answer
here, and this is the constraint that most often makes the whole thing not worth it.
2. CSS isolation. One document, one cascade. Team A's .button rule restyles Team
B's buttons. The fixes: CSS modules or CSS-in-JS with generated class names, a
mandatory prefix convention, or shadow DOM (which gives real isolation and makes global
theming, focus management and third-party component libraries harder).
3. Routing and shared state. Who owns the URL? Cross-fragment communication needs a contract, and the workable one is a custom-event bus or a shared store with a versioned interface. Direct imports across fragments recreate the coupling you removed.
// A deliberately thin contract: events with a versioned payload.
// Fragments never import each other's modules.
window.dispatchEvent(new CustomEvent('cart:item-added', {
detail: { v: 1, sku: 'ABC-123', qty: 2 },
}));
4. Bundle size. Each remote ships its own dependency graph. Three fragments each using a date library, a form library and an icon set means three copies unless every one is shared, and every shared package is another version-coordination point. Measured regressions of 30 to 50 percent in total transferred bytes are common and are the thing to instrument from day one.
The alternatives, which are usually better
Before recommending micro-frontends, exhaust these, because each gives some of the benefit at a fraction of the cost:
Monorepo + clear module ownership + CODEOWNERS
-> Code isolation, single deploy. Solves "whose code is this".
Monorepo + affected-only builds (Nx, Turborepo, Bazel)
-> Cuts CI time hugely, which is usually the real complaint.
Trunk-based development + feature flags
-> Decouples deploy from release, so a team ships without a release
train. Solves most of the coordination pain directly.
See: deploy is not release.
Independent deployment per ROUTE, not per component
-> /checkout/* is a separately deployed app behind one CDN and one
design system. Coarse-grained, and it keeps the runtime simple
because there is no shared page.
That last one is the underrated answer, and it is what I would propose in most cases where micro-frontends get raised: route-level splits give real deploy independence with none of the shared-cascade, shared-React, shared-DOM problems, because two routes never render at the same time.
A worked example
A retail platform. 60 frontend engineers across 8 teams, one React monolith, one deploy pipeline. The proposal on the table is Module Federation across 8 fragments.
Step 1: measure the coordination cost, so the decision is arithmetic.
CI pipeline: 34 minutes (build 19, test 11, deploy 4)
Merge queue p50: 2.1 h, p95: 6.4 h
Release cadence: 2 per day, on a shared train
Rollbacks last quarter: 11, of which 7 blocked other teams' changes
Time from PR approved to production, p50: 5.2 h
Coordination overhead, estimated:
7 blocking rollbacks x ~8 teams x ~1 h of blocked work = ~56 engineer-hours
Merge queue wait: 60 engineers x ~3 PRs/week x 2.1 h = ~378 h/week of
wall-clock delay, though not of blocked engineer time
The merge-queue number is the one that hurts, and notice it is a wall-clock cost rather than a fully blocked-engineer cost, which is the honest framing.
Step 2: cost the alternatives against the same numbers.
Option A: affected-only builds (Nx or Turborepo)
Effort: ~3 engineer-weeks
Expected: CI 34 min -> ~9 min for a typical single-team change
merge queue p50 2.1 h -> ~35 min
Risk: low. Reversible.
Option B: trunk-based + feature flags, remove the release train
Effort: ~6 engineer-weeks (flag platform, policy, CI enforcement)
Expected: teams deploy on merge; blocking rollbacks mostly disappear
because a rollback becomes a flag flip
Risk: low-medium. Flag debt needs governance.
Option C: route-level split (checkout, search, account as separate apps)
Effort: ~10 engineer-weeks
Expected: 3 of 8 teams fully independent; no shared runtime, because
routes never co-render
Risk: medium. Duplicate design-system bundle per route, ~40 KB each.
Option D: Module Federation, 8 fragments on shared pages
Effort: ~6 engineer-months, plus ongoing platform ownership (~1 FTE)
Expected: all 8 teams deploy independently
Risk: high. React singleton means coordinated upgrades anyway.
Measured bundle growth in comparable migrations: 30-50%.
New failure mode: a remote's CDN failing degrades the page.
Step 3: sequence rather than choose. A and B first, because they are 9 weeks combined against 6 months, and they address the measured pain directly. Then re-measure. If the merge queue is 35 minutes and rollbacks no longer block other teams, the case for D has largely evaporated.
What actually happened in cases shaped like this: A and B removed most of the pain, C was applied to checkout only, because checkout genuinely needed a different release cadence for compliance reasons, and D was never needed. That is the common outcome and it is worth saying plainly: the coordination problem is usually a pipeline problem wearing an architecture costume.
When I would go to D: if there were 8 teams whose work genuinely co-renders on the same page (a dashboard composed of independently-owned widgets), or an acquisition where two organisations run different frameworks and merging them is not on the table, or a genuine organisational boundary where one fragment is built by a different company. Those are real, and Module Federation is the right answer for them.
Production evidence
Spotify's "Backstage" plugin architecture composes independently-developed plugins into one developer portal, which is the case micro-frontends fit well: many independently-owned widgets on shared pages, with a plugin contract.
IKEA and Zalando have both published on fragment-based composition, Zalando's "Mosaic" being an early server-side composition framework. Zalando subsequently moved away from it toward a more consolidated approach, which is a useful data point about the operational cost rather than an argument that it never works.
Module Federation shipped in Webpack 5 (2020) and is now the dominant runtime
mechanism, with implementations in Rspack and Vite. Its shared and singleton
configuration is the codified acknowledgement that dependency version coordination is
the hard part.
Amazon Prime Video's 2023 write-up on consolidating a distributed service into a monolith is often cited here; it is about backend services rather than frontends, so quoting it as micro-frontend evidence is a mistake worth avoiding. Segment's "Goodbye Microservices" is the better-fitting cautionary reference for over-decomposition generally.
Martin Fowler's micro-frontends article (Cam Jackson, 2019) is the canonical description and is notably even-handed about the costs, particularly bundle duplication and the CSS isolation problem.
The debate
The case for: with enough teams, deployment coupling is a genuine tax that compounds, and independent deployment is the only structural fix. For organisations where fragments are built by genuinely separate groups (different companies, an acquisition, a plugin ecosystem), there is no alternative. And route-level splitting in particular is cheap and effective.
The case against: the frontend shares one DOM, one cascade, one global scope and one memory space, so the isolation is much weaker than the microservices analogy implies. Shared dependency singletons reintroduce the coordination you were removing. Bundle size regresses measurably. And you acquire a platform team, permanently, to own the composition layer.
My position: the honest answer is "usually not, and here is what to do instead", and the test is whether teams' work co-renders on the same page. If teams own separate routes, split by route: real deploy independence, no shared runtime, no cascade conflicts, because two routes never render simultaneously. If teams own separate widgets on the same page, that is the genuine micro-frontend case and Module Federation earns its cost.
Before either, I would fix the pipeline, because in most organisations that raise this, the measured pain is merge queue depth and a shared release train, and affected-only builds plus trunk-based development with feature flags addresses both in weeks rather than months. Recommending an architecture change for a pipeline problem is the specific failure mode here.
The threshold I would use: below roughly 5 teams, no. Above 10 teams with genuinely co-rendering ownership, yes. In between, split by route and fix the pipeline. And I would insist on instrumenting total transferred bytes from day one, because the bundle regression is real, it is gradual, and nobody notices it until the mobile conversion rate moves.
Follow-up Q&A
"When are micro-frontends worth it?" When independent deployment is the actual constraint and teams' work co-renders on the same page. If teams own separate routes, route-level splitting gives the same deploy independence with none of the shared runtime problems, because two routes never render at once. Below about five teams the coordination cost does not justify it. Above ten, with widgets from different teams on one page, it does. And before either, I would check whether the real complaint is merge queue depth and a shared release train, because affected-only builds and trunk-based development with flags fix that in weeks instead of months.
"What's the hardest part in practice?" Shared dependency versions. Module
Federation's singleton: true gives you one React across all fragments, which means
every team upgrades React together, which is exactly the coordination you were removing.
Drop the singleton and you ship multiple React copies and hooks break across the
boundary, because two React instances do not share the internal dispatcher. There is no
clean resolution, and it is the constraint that most often makes the whole thing not
worth it.
"What about CSS?" One document, one cascade, so team A's .button rule restyles
team B's buttons. The options are CSS modules or CSS-in-JS with generated names, a
mandatory prefix convention enforced in CI, or shadow DOM. Shadow DOM gives real
isolation and costs you global theming, cross-boundary focus management, and
compatibility with component libraries that assume a single document. I would take
generated class names plus a linted prefix for most cases.
"What's the bundle cost?" Each remote ships its own dependency graph, so three fragments each pulling a date library, a form library and an icon set means three copies unless each is explicitly shared, and every shared package is another version coordination point. Regressions of 30 to 50 percent in total transferred bytes are commonly reported. It is gradual, so nobody notices until a mobile conversion metric moves, which is why I would put a byte budget in CI from day one.
"What new failure modes appear?" Runtime composition means the page takes a dependency on another team's CDN being reachable during the user's session, so every remote needs an error boundary and a degraded fallback. Version skew becomes possible: a shell deployed at 10am composing a remote deployed at 3pm, with no build-time check that they agree. And debugging crosses ownership boundaries, so an error in production may be in code your team cannot read, which is an on-call and observability problem as much as a technical one.
"What would you do instead, concretely?" Three things in order. Affected-only builds with Nx, Turborepo or Bazel, which typically takes a 30-minute pipeline to under 10 for a single-team change and is about three engineer-weeks. Trunk-based development with feature flags, which removes the release train so a rollback is a flag flip rather than a blocking redeploy. And route-level splitting for any team that genuinely needs a different release cadence. That is roughly nine to nineteen weeks against six months for full Module Federation, and it addresses the measured pain rather than a proxy for it.
Common misconceptions
"It's microservices for the frontend." Backend services are isolated by process and network. Frontend fragments share a DOM, a cascade, a global scope and a heap. The isolation is much weaker and that is where the cost lives.
"It makes builds faster." Each fragment builds faster; the system does not necessarily get faster to change, and affected-only builds in a monorepo achieve the same thing without the runtime complexity.
"Teams become fully independent." Shared singletons, the design system, the router and the authentication contract all remain coordination points.
"Users won't notice." Bundle duplication is measurable and it lands on mobile users first.
"It's all or nothing." Route-level splitting is a legitimate middle point and it is the right answer far more often than full runtime composition.
Interview delivery note
Give the test rather than a list of pros and cons, because the test is what demonstrates judgement: "The question I'd ask is whether the teams' work co-renders on the same page. If teams own separate routes, split by route: real deploy independence, no shared cascade, no shared React, because two routes never render at the same time. If teams own separate widgets on one page, that's the genuine micro-frontend case and Module Federation earns its cost."
Then the reframe that usually applies: "but before either, I'd check what the measured pain actually is. In most organisations that raise this it's merge queue depth and a shared release train, and affected-only builds plus trunk-based development with feature flags fix both in about nine weeks against six months. Recommending an architecture change for a pipeline problem is the specific failure mode here."
The depth signal is naming the constraint nobody mentions: "and the hard part is the
shared dependency singleton. singleton: true means one React across all fragments,
so every team upgrades together, which is exactly the coordination you were trying to
remove. Drop it and you ship two Reacts and hooks break across the boundary. There's no
clean answer, and that's usually what decides it."
Close with a threshold, because a position with a number is stronger than a position without one: "below about five teams, no. Above ten with genuinely co-rendering ownership, yes. In between, split by route and fix the pipeline."
Further reading
- Cam Jackson, "Micro Frontends" on martinfowler.com (2019), the canonical description including the composition strategies and their costs.
- Webpack's Module Federation documentation, particularly the
sharedandsingletonsemantics. - Zalando's Mosaic write-ups and their later consolidation, for an organisation that ran server-side composition at scale and changed its mind.
- Spotify's Backstage plugin architecture documentation, for the case where the pattern fits well.
- The Nx and Turborepo documentation on affected-only builds, for the alternative that addresses the usual root cause.