This series walks through an actual enterprise microfrontend platform, end to end: one Host shell, three shared platform microfrontends, a manifest-driven mechanism for mounting any number of independently-owned domain microfrontends, a full OIDC auth flow, and a CI/CD pipeline. Every code snippet in this series is real and traceable to the actual boilerplate it's built from, on GitHub.
Part 1 is the decision everything else in this series depends on: why Module Federation, and not one of the two other credible options.
The one requirement that rules everything else out
Strip away the buzzwords, and an "enterprise microfrontend platform" only has to guarantee one thing: a team ships a change to their part of the app without anyone else redeploying anything. Not "in theory, with enough coordination" — actually, mechanically, true. If shipping one team's bug fix requires a platform team to cut a release, this isn't microfrontends — it's a monolith with extra steps.
That single requirement rules out more than it looks like it should. It rules out compiling every team's code into one shared build (that's just a single-page app with more steps). And it rules out anything where the Host app needs to know, at its own build time, which teams' pages exist and which version of each — because "known when the Host was built" and "deployed independently of the Host" are opposites.
The decision
Use Webpack 5 Module Federation, in runtime-composition mode, as the platform's way of putting every team's page together into one app:
- The Host ships with an empty list of remote apps built in. Instead, it looks up every team's page from a small list — a manifest — that it fetches fresh every time the app loads.
- React, the shared state layer, and the shared design system are all declared as singletons: every team's page gets the exact same running instance of each, not its own separate copy.
- "Deploying" a team's page means adding or updating one entry in that manifest. The Host itself is never rebuilt.
Here's that decision as it actually exists in the repo, not as illustrative pseudocode:
// apps/host/webpack.common.js
module.exports = createModuleFederationConfig({
federation: {
name: 'host',
// remotes stay empty at build time — resolved at runtime from the
// manifest. This is what makes a domain MFE's deploy a manifest
// update instead of a Host rebuild.
remotes: {},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0 || ^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0 || ^19.0.0' },
'@org/store': { singleton: true, requiredVersion: `^${storePkg.version}` },
'@org/components': { singleton: true, requiredVersion: `^${componentsPkg.version}` },
},
},
...
});
The Host doesn't import a single line of any team's code. At runtime, it fetches a small JSON file — platform.manifest.json — that maps each page's route to the URL its bundle lives at, and mounts whatever's listed there:
// packages/runtime-module-federation/src/manifest-loader.ts
export async function loadDomainManifest(config: PlatformConfig): Promise<ManifestEntry[]> {
if (cached) return cached;
const res = await fetch(config.manifestUrl);
if (!res.ok) throw new Error(`Failed to load manifest from ${config.manifestUrl}: ${res.status}`);
cached = (await res.json()) as ManifestEntry[];
return cached;
}
This is the single most important decision in the whole platform: deploying a team's page means updating one entry in this manifest, not rebuilding the Host. Part 5 covers the script that does that update safely.
The other options on the table
Two other approaches genuinely fit this same problem, and it's worth being clear about why each lost out here rather than just naming Module Federation as a given:
| Approach | How it works | Why it's not the default here |
|---|---|---|
| Build-time composition | The Host imports every team's code directly and bundles it all into one build, like a normal app with more folders. | A team's deploy would require rebuilding and redeploying the Host — exactly the coupling this platform exists to avoid. Ruled out outright, not just deprioritized. |
| Next.js Multi-Zones | Each team ships an independently deployable Next.js app; a router at the edge stitches them together by URL prefix. | The right call if real SSR/SEO is a hard requirement. But each zone is its own separate page, not a widget sharing one React tree with the others — so login state, shared components, and shared data don't automatically carry across zones the way they do here. |
| single-spa | A framework-agnostic runtime composition tool — any framework's app can mount alongside any other. | Makes real sense if an org runs Vue, Angular, and React side by side. This platform assumes every team is on React, so that extra flexibility is a cost being paid for a problem the platform doesn't have. |
| Module Federation (chosen) | Webpack loads each team's bundle at runtime and shares one running copy of React, the shared state layer, and the design system across all of them. | Meets the independent-deploy requirement with the least new infrastructure, and keeps every team's page working inside one shared, logged-in app. |
If real SSR/SEO ever becomes a hard requirement later, switching to Multi-Zones or single-spa is a contained swap — it changes how the Host and the deploy pipeline work, not the rest of the platform.
What this buys, and what it costs
What it buys:
- A team ships by adding one entry to the manifest — no pull request against the Host, no waiting on a platform team's release train.
- Every team's page uses the same running copy of React, the same design system, and the same logged-in session — not a slightly different copy per team.
- Nothing about this approach locks the platform into one cloud provider, CI tool, or hosting vendor.
What it costs — two real trade-offs, both handled later in this series:
- The manifest becomes critical. Every team's page is found the same way: by looking it up in that one shared list. If wherever that list lives goes down, no page loads — even pages that changed nothing today.
- A version mismatch can slip through unnoticed. Each team declares which version of React (and the shared libraries) their page was built against. Nothing stops that declaration from quietly drifting out of sync with what's actually running — the mismatch only shows up once a real user hits it in a real browser, not when the code is built.
Both of those get a concrete, working mitigation later in this series — not just a warning label.
Running several teams' pages together locally is also a bit more involved than running one single app — this platform uses a monorepo tool (Turborepo) to keep that workable, though it isn't strictly required for Module Federation to work.
That's the shape of this series: the decision in this part, and then each following part is the architecture that decision leads to — the topology it produces, how login is unified across every team's page, the rules that keep it reliable, and how the whole thing actually ships. Next up: the platform's layout, the Host shell, and the three shared microfrontends every team builds against.
Next: Part 2 — Inside the Platform: Topology, Host Shell & Platform MFEs
Top comments (0)