Skip to content
Barnett Studios
20 March 2026 · Guide · Lyubomir Bozhinov · 4 min read

Micro-Frontends: When to Split and When to Stay

Micro-frontends solve a real problem — but only if you actually have that problem. A practical guide to when the split is worth it, what the architecture really costs, and the pain points nobody warns you about.

The moment you have four teams shipping features to the same React app, you feel the friction before you name it. Merge conflicts in shared components. Broken builds from unrelated changes. Deploys that require coordinating five people’s calendars. Micro-frontend architecture solves all of that. It also introduces problems you won’t anticipate — and those problems are the ones worth talking about.

When splitting is the right call

Micro-frontends solve an organisational problem, not a technical one. If you have one team building one product, a monolithic frontend is simpler, faster, and cheaper to maintain. Full stop.

The split becomes worth it when you have independent teams that need to ship independently. Conway’s Law applies directly — your system architecture will mirror your team structure whether you plan for it or not. Sam Newman, in Building Microservices, turns this into a design tool: align your architecture to your team boundaries deliberately, not by accident. If your payments team and your reporting team share a release cycle and a deployment pipeline, you don’t need micro-frontends. If they don’t — if one team’s bug fix is blocked by another team’s half-finished feature — you probably do.

The other trigger is deployment isolation. Consider a charting module that wraps a third-party library with its own release cadence and breaking changes. Isolating it as a remote means upgrading that dependency without touching — or risking — the rest of the application. One remote, one concern, one deploy.

What the architecture actually looks like

Module Federation is the dominant pattern — Fowler documented the broader micro-frontends concept in 2019, and Module Federation gave it a practical runtime mechanism. It works across bundlers — Webpack, Vite, Rspack — and the architecture is the same regardless of which you choose. A shell application acts as the host, loading remote applications at runtime via their remoteEntry.js entry point. Each remote exposes a root component. The shell renders them.

In a more complex setup — several remotes, each on its own port in development, each deployed to a versioned CDN path in production — the shell resolves remote URLs based on the environment. Local development hits localhost:{port}, staging hits cdn-dev.example.com/{app}/latest/remoteEntry.js, production hits cdn.example.com/{app}/v2.1.0/remoteEntry.js. The version pinning matters. You don’t want production silently picking up a latest tag that someone pushed during lunch.

In a simpler setup — one remote, two ports — the architecture is identical in kind, just smaller in scope. Same plugin, same pattern, same shared dependency config. The difference is operational complexity, not conceptual complexity.

The pain points nobody warns you about

React Context doesn’t cross MFE boundaries by default. This is the one that catches everyone. If your shell creates an AuthContext and your remote creates its own React instance, the remote can’t see the shell’s context without some additional effort. One possible fix is a shared library — marked as a singleton and loaded eagerly — that exports your contexts. Every app imports from the same package, which means they share the same React instance and the same context objects. Get this wrong and you’ll spend days debugging why your remote app can’t read the authenticated user.

Manual chunk splitting will break your shared dependencies. Your bundler’s default chunking is aggressive, and the temptation is to configure manual chunks for performance. Don’t. Module Federation relies on its own dependency resolution for shared modules. If your manual chunk configuration splits React into a separate chunk before Module Federation can mark it as shared, you end up with duplicate React instances — and duplicate instances mean broken contexts, broken hooks, and errors that only appear in production builds. Whether you’re on Rollup, Rolldown, or Webpack — let the automatic chunking handle it.

Your shared dependency list will grow until it hurts. The lean system started with fewer than five shared dependencies. The complex one had nearly forty. Every shared dependency is a coordination point — a version that every remote must agree on, a package that can’t be independently upgraded. The rule: share what must be shared (React, your context library, your design system) and nothing else. If a remote needs a charting library that no other remote uses, let it bundle its own copy.

Testing requires disabling Module Federation entirely. Your test configuration should alias remote imports back to source modules and skip the federation plugin. You’re not testing the federation — you’re testing the components. Vitest with jsdom, standard React Testing Library patterns, no runtime module resolution. Keep your test setup honest about what it’s actually exercising.

The decision framework

Split when teams need independent deploy cycles. Split when a dependency’s upgrade risk justifies isolation. Split when merge conflicts across teams are costing more than the federation overhead.

Stay monolithic when you have one team, one product, one deploy pipeline. Stay monolithic when the “micro-frontend” would be one remote with one component — the overhead isn’t justified.

And if you do split: start with one remote. Solve the context sharing problem, the shared dependency problem, and the deployment pipeline problem for that one boundary. Then — and only then — add the second.