Nguyen Le Phong

Foundations of Software ArchitecturePart 6 of 6

Micro-Frontends: When They're Worth It, and What They Cost

Splitting the frontend into independently deployable pieces can free teams — or bury you in complexity. Written from shipping an Angular-host + React-modules setup at scale: when micro-frontends pay off, and the bill they come with.

Picture a repository that started as a tidy Angular app for twenty developers and, three years later, has fifty-plus engineers committing into it every day. Every pull request touches the same bundle. A bug fix on the checkout flow accidentally breaks the onboarding wizard because they both import a shared utility that someone quietly changed. The release calendar looks like a game of Tetris — five teams lining up to get their feature into the one scheduled deploy window. Everyone's waiting on everyone else, and nobody ships as fast as they want to.

That's the pain that makes frontend architects start whispering the phrase micro-frontends. The idea sounds seductive: if microservices freed backend teams from the monolith, why can't we do the same for the UI? Give each team its own independently deployable frontend slice, and let the browser (or a server) stitch them back into one seamless product.

But the analogy cuts both ways. Microservices brought genuine freedom — and genuine operational pain. Micro-frontends are no different. Having shipped an Angular host with React-powered feature modules at a multi-team company, I want to give you the honest version: what worked, what hurt, and the questions you should answer before you start splitting.

What micro-frontends are

The term was popularised around 2016–2017 as teams noticed that the microservices revolution hadn't reached the browser. In a classic single-page application, one monolithic frontend bundle owns everything: routing, state, every component, every team's features. Micro-frontends apply the same decomposition idea to the UI layer.

A micro-frontend is an independently buildable, independently deployable piece of UI, owned end-to-end by one team. That team controls its own repository (or at least its own build pipeline), its own release cycle, and its own technology choices within agreed boundaries. At runtime, a shell application — sometimes called the host or container — composes these pieces into the unified product the user sees.

The key shift is ownership. In a monorepo with shared code, a change to a shared utility can break any team's feature; coordination is constant. With micro-frontends, Team A's deployment of the checkout module doesn't require a sign-off from Team B's dashboard team. The contract between them is the agreed-upon integration interface, not shared source code.

A shell/container app at the top composes independently deployed frontend modules from Team A, Team B, and Team C into one page in the browser. Shell / Container App routing · auth · shared nav · orchestration TEAM A Checkout MFE React · own deploy TEAM B Dashboard MFE Angular · own deploy TEAM C Profile MFE Vue · own deploy v2.4.1 · deployed v1.9.0 · deployed v3.1.2 · deployed BROWSER — composed at runtime into one seamless product
The shell app owns routing, authentication, and the global nav. Each team ships its own micro-frontend on its own cadence. The browser assembles them into a single experience — the user sees one product, even though three teams deployed independently.

Integration approaches

"Micro-frontends" is a family of techniques, not a single technology. How you stitch the pieces together has enormous consequences for team autonomy, performance, and operational complexity. Here are the five main approaches:

ApproachHow it worksKey trade-off
Build-time integration (npm packages) Each micro-frontend is published as a versioned npm package. The shell installs them as dependencies and bundles everything together. Simple to set up, but teams must coordinate releases — updating a remote means the shell has to re-deploy. Not truly independent.
Server-side composition A reverse proxy or edge layer (e.g. Nginx, a Next.js edge function, Zalando's Tailor) fetches HTML fragments from different services and assembles them before the response reaches the browser. Great for SEO and first-paint; adds infrastructure complexity and a new latency surface between services.
Run-time via Module Federation Webpack 5 (or Rspack/Vite equivalents) lets builds expose modules that are loaded by the shell at runtime from separate CDN URLs, without re-bundling. True independence — remotes deploy without touching the shell — but version skew, shared dependency negotiation, and debugging tooling are hard.
iframes Each micro-frontend runs in its own iframe. Isolation is near-perfect; communication is via postMessage. Rock-solid isolation and security boundary, but terrible UX (scroll, focus, deep-links, accessibility all require custom plumbing); feels dated.
Web Components Each micro-frontend exposes a custom HTML element (<checkout-app>). The shell drops the element into the page; the browser handles lifecycle. Framework-agnostic by design, but SSR support is immature, and complex shared state still requires a global event bus or context solution.

In practice, Module Federation has become the dominant choice for large teams wanting genuine runtime independence. The Webpack 5 plugin that ships it has been ported to Rspack and has Vite equivalents (e.g. @originjs/vite-plugin-federation). When someone today says “we’re doing micro-frontends,” they almost always mean Module Federation.

Tip: start with server-side composition if SEO matters

If your app must rank in search results and you're hitting the micro-frontend idea for the first time, server-side composition gives you the cleanest HTML before the browser touches it. Module Federation is the power move, but it requires a more mature deployment story before it pays off.

Why teams adopt them

The motivations for going micro-frontend are consistent across the companies I've seen. They almost always trace back to one of four pressures:

  • Independent deploys. The single most cited reason. When fifteen teams share one frontend build, a deploy is a coordination tax — everyone waits for everyone else's work to clear QA. Micro-frontends let Team A ship on Tuesday without waiting for Team B's half-finished feature to be ready. Release frequency goes up; blast radius per release goes down.
  • Team autonomy and ownership. A team that can build, test, and deploy their slice end-to-end moves faster and feels more accountable. “Full-stack ownership” becomes real when the frontend isn't a shared commons that everyone trips over.
  • Incremental technology migration. This is underrated. If you're running a five-year-old AngularJS app and want to move to React, you can't rewrite the whole thing at once. Micro-frontends let you replace one route at a time with a new React module while the rest of the AngularJS app keeps running. It's the strangler-fig pattern, applied to the frontend.
  • Fault isolation. A JavaScript runtime error in the checkout micro-frontend crashes the checkout module, not the whole app. The shell can catch the error boundary, show a fallback, and let the user navigate away. In a monolith, an uncaught error often tears down the entire page.

The real costs

Here is where many blog posts go quiet. Micro-frontends carry a genuine bill, and you should see it clearly before you sign up.

Bundle and dependency duplication. If three micro-frontends each bundle their own copy of React, the user downloads React three times. Module Federation's shared scope mechanism can deduplicate at runtime, but configuring it correctly across teams — especially when versions drift — is not trivial. A naive micro-frontend architecture can actually bloat your payload compared to a well-optimised monolith.

Sharing a design system. A unified look and feel demands a shared component library. If that library is consumed at build time (npm), every micro-frontend that updates it must be redeployed. If it's consumed at runtime via Module Federation, you need a versioning and compatibility strategy. Either way, the design system becomes a cross-team coordination point — the very thing you were trying to eliminate.

Global state and cross-MFE communication. Micro-frontends are supposed to be independent, but users are holistic. A notification badge in the shell needs to know about an action in the checkout module. An authentication event in one module must propagate everywhere. Solutions range from a shared Redux store (tight coupling) to a custom event bus or a shared context object on the global window — all of which are footguns that require discipline to use safely.

Version skew. Because teams deploy independently, at any given moment the shell might be running a version of Module Federation that expects remote entry format v1, while Team C's latest deployment emits format v2. These mismatches surface as cryptic runtime errors that are hard to reproduce locally and even harder to trace in production. You need explicit compatibility contracts and graceful degradation strategies.

Runtime performance. Multiple async bundle loads, runtime module negotiation, waterfall fetches for remote entries — micro-frontends introduce latency that a monolith, with everything statically bundled, simply doesn't have. Careful preloading, a CDN strategy, and HTTP/2 push all help, but they require active investment.

Heavier operations and observability. You now have N deployment pipelines, N sets of CDN cache-busting rules, N build configurations, and N sets of error logs to correlate when something goes wrong across module boundaries. The DevOps overhead is real. You'll want distributed tracing, structured logging that includes the micro-frontend version, and a shared alerting dashboard before you go to production.

The complexity always goes somewhere

Breaking a monolith into independent pieces doesn't reduce total complexity — it moves complexity from the source code into the integration layer, the operational tooling, and the cross-team contracts. If your organisation isn't ready to invest in that operational infrastructure, you'll end up with a distributed monolith: all the coupling of a monolith, plus all the operational overhead of a distributed system.

Shipping Angular-host + React modules: the honest account

A couple of years ago I was part of the team that took a large Angular platform — several hundred thousand lines, five product squads, a quarterly release cadence that felt more like a hostage situation than a product process — and migrated it to a Module Federation architecture: an Angular shell hosting independently deployed React feature modules.

Here's what genuinely worked.

  • Independent deploy cycles changed the culture. Within two months, squads that had historically shipped quarterly were shipping every week. The feedback loop compressed. Engineers started caring more about observability because they were the ones watching the dashboard after their own deploy went out.
  • Feature isolation reduced regression surface dramatically. A change to the billing module no longer had a theoretical path to breaking the user settings page. Our regression test suite shrank in scope per team; integration tests focused on the contract points rather than the whole app.
  • The incremental migration was a lifesaver. We didn't have a six-month “big bang” rewrite freeze. React modules replaced Angular views one route at a time, over eighteen months, while the product kept shipping. Business stakeholders barely noticed the transition — which is exactly what you want.

Here's what hurt.

  • Shared state was a constant headache. We ended up with a custom event bus on the global window object — functional, but fragile. When two teams independently changed the event payload shape, we got silent bugs that only appeared in specific navigation sequences. We eventually modelled cross-MFE events like an internal API, with a schema registry, which helped enormously but cost weeks to build.
  • Duplicate dependencies inflated the bundle. Getting Module Federation's shared configuration right across Angular and React — two different build systems — took real effort. For several sprints, users were downloading two copies of RxJS and nearly two copies of a charting library before we caught it. Automated bundle analysis in CI became essential.
  • Version coordination never fully went away. The shell's Angular version and the React modules' dependency matrix had to stay loosely compatible. When Angular released a major update, we couldn't just upgrade the shell; we had to audit every module's peer dependencies first. The coordination cost was lower than the monolith, but it didn't disappear.

The net verdict: the architecture was the right call for that organisation at that scale. But if I'm honest, we underestimated the operational investment by about thirty percent, and the shared-state problem took twice as long to solve cleanly as we projected. Go in with open eyes.

When NOT to use micro-frontends

The clearest sign that micro-frontends are wrong for your situation is when the main source of frontend pain is not team-scale coordination — it's something else entirely.

  • A single small team. If everyone on the frontend can fit in one video call, you don't have a coordination problem. You have a codebase to organise. Micro-frontends would give you all the operational overhead with none of the team-autonomy benefit. Use a modular monolith frontend with well-enforced module boundaries instead.
  • A small application. If the entire frontend is a few routes and a few thousand lines, splitting it up creates more infrastructure surface area than the product itself. The overhead of Module Federation's configuration and multi-pipeline CD far outweighs any benefit.
  • When a monorepo with good boundaries would do. Nx, Turborepo, and similar tools let you enforce strict module boundaries, run builds only for affected packages, and give teams a degree of ownership — without the runtime composition complexity. For many companies in the 20–80 engineer range, a well-structured monorepo is the better trade-off. It's also dramatically easier to refactor.
  • When your team isn't yet comfortable with the basics. If your CI/CD pipeline is unreliable, your observability story is thin, or there are unresolved cross-team communication dysfunctions, adding micro-frontend complexity will amplify all of those problems. Fix the foundation first.

This is closely analogous to the backend decision. Not every backend should be a fleet of microservices — and the same reasoning applies here. If you haven't read through the Monolith → Microservices breakdown, the questions it asks before splitting a backend apply almost verbatim to the frontend: do you have independent scaling needs, independent failure domains, and genuinely autonomous teams? If not, the monolith — or the modular monolith — is probably the better answer.

The modular monolith sweet spot

A well-organised frontend monorepo with strict linting boundaries, per-team owned packages, and a fast incremental build system gives you 70–80% of the autonomy benefit at a fraction of the operational cost. Before you reach for Module Federation, ask whether cleaner module boundaries in your current codebase would solve the actual problem.

By company size

One of the most useful lenses for this decision is simply: how many frontend engineers do you have, and how independently do their teams need to operate?

Organisation sizeTypical frontend team structureRecommendationReal-world example pattern
Small (< 15 FE devs) One or two squads, everyone knows everyone's code. Don't. Use a modular monolith or a well-structured monorepo. The coordination overhead of micro-frontends exceeds the benefit. Early-stage SaaS startups: Next.js app with feature folders and strict imports — ships fast, easy to reason about.
Mid-size (15–60 FE devs) Three to eight squads, starting to feel deploy contention and ownership friction. Maybe, at the seams. Consider extracting only the highest-friction boundaries — e.g. a checkout, a reporting dashboard — rather than splitting everything. A monorepo with build caching may solve it first. Scale-up e-commerce: monorepo for most features, a Module Federation remote only for the payments flow (PCI-isolated team, separate deploy).
Large / enterprise (60+ FE devs) Many squads, often across time zones, with hard team ownership boundaries and separate roadmaps. Often justified. The coordination overhead of a shared deploy is real and measurable. Module Federation or server-side composition with a mature platform team is a sensible investment. Large banks, telcos, global retail: shell app owned by a platform team; dozens of micro-frontends owned by feature tribes, each with its own CD pipeline and version contract.

Spotify's engineering blog has documented their “squad model” and the frontend architecture decisions that came with it. Zalando has written openly about their Tailor server-side composition approach. IKEA's digital platform teams have discussed Module Federation at scale. What these companies have in common is not the technology — it's the scale: many autonomous teams with genuinely independent roadmaps who would otherwise block each other daily.

At the other end of the spectrum, some of the most productive frontend organisations I've seen in the 20–50 developer range run everything in a single Nx monorepo with strictly enforced module boundaries and a fast incremental CI. They get fast builds, clear ownership, and zero runtime composition bugs — because there's no runtime composition. They revisit the micro-frontend question every six months as the company grows, and that's exactly right.

Key takeaways

  • Micro-frontends apply the microservices decomposition idea to the UI: independently buildable, independently deployable frontend slices, each owned by one team, composed at runtime (or build time, or server-side) into one product.
  • Module Federation (Webpack 5 / Rspack / Vite) is the dominant runtime approach — it provides true deploy independence but requires careful shared-dependency configuration and version contract management.
  • The core benefits are deploy independence, team autonomy, incremental framework migration, and fault isolation. If you don't genuinely need those at scale, you're paying the cost without the reward.
  • The real costs are bundle duplication, shared design system governance, cross-MFE state complexity, version skew, runtime latency, and heavier DevOps. None of these are fatal — but all of them require active investment.
  • Do not use micro-frontends if you have a small team, a small app, or if a well-structured monorepo would resolve the actual friction. The modular monolith is underrated.
  • Match the architecture to the team topology: small teams → modular monolith; mid-size → monorepo or surgical MFE splits; large enterprise → full micro-frontend platform with a dedicated platform team.
  • The complexity always goes somewhere. Micro-frontends move it from the source code into the integration contracts and operational infrastructure. Make sure your organisation is ready to own that shift before you commit.

This article is part of a series on frontend and backend architecture. If you landed here first, I'd encourage you to read back to where the series starts — Ports & Adapters — which covers the hexagonal architecture pattern and how to keep your core business logic clean and testable, regardless of the framework or delivery mechanism sitting around it. The ideas are complementary: the same thinking that puts clear boundaries inside a single backend service applies, at a larger grain, to the boundaries between frontend teams. More architecture writing is coming — sign up or check back soon, and I hope to see you there.