Skip to content

Commit 129b0ae

Browse files
Khanh DaoKhanh Dao
authored andcommitted
update
1 parent 3626875 commit 129b0ae

4 files changed

Lines changed: 430 additions & 1 deletion

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
---
2+
title: 'Micro-Frontends with Module Federation'
3+
description: 'How to reason about micro-frontend architecture and Webpack/Rspack Module Federation as a staff engineer, with working config examples and interview framing.'
4+
---
5+
6+
# Micro-Frontends & Module Federation — Staff Engineer Guide
7+
8+
## Interview framing
9+
10+
```txt
11+
1. What organizational problem does this solve? (Conway's Law, team autonomy)
12+
2. What does it cost? (shared dependency management, runtime coupling, debugging complexity)
13+
3. Would you actually recommend it for THIS team/product, or is it overkill?
14+
4. How do you keep independently-deployed pieces from breaking each other at runtime?
15+
```
16+
17+
The strongest answers start with the organizational driver, not the technology:
18+
19+
> "Module Federation is a _solution to a team-topology problem_ — many teams shipping to one product on independent timelines. If there's one team and one deploy cadence, I would not reach for it; a monolith with good code-splitting solves the same technical problem with far less operational cost."
20+
21+
---
22+
23+
# 1. The Core Problem It Solves
24+
25+
```txt
26+
Monolithic SPA Micro-frontend (Module Federation)
27+
─────────────── ─────────────────────────────────
28+
One build Independent builds per team
29+
One deploy pipeline Independent deploy pipelines
30+
One team can block all others Teams ship on their own cadence
31+
Shared bundle grows unbounded Each remote loaded on demand
32+
Single point of build failure A broken remote degrades, not blocks
33+
```
34+
35+
Module Federation (native to Webpack 5, also supported by Rspack/Vite plugins) lets a JavaScript application dynamically load code from a **separately built and deployed** bundle at runtime, sharing dependencies (React, design system, etc.) instead of duplicating them.
36+
37+
Two roles:
38+
39+
- **Host** (a.k.a. shell/container) — the app the user loads first; it decides which remotes to pull in.
40+
- **Remote** — an independently built/deployed app that exposes modules the host can consume.
41+
42+
A remote can itself be a host to other remotes — federation composes.
43+
44+
---
45+
46+
# 2. Code Example: Host + Remote Setup
47+
48+
### Remote (`team-checkout`, exposes a component)
49+
50+
```javascript
51+
// checkout-app/webpack.config.js
52+
const { ModuleFederationPlugin } = require('webpack').container;
53+
54+
module.exports = {
55+
mode: 'production',
56+
devServer: { port: 3001 },
57+
output: {
58+
publicPath: 'https://checkout.mycdn.com/', // must be resolvable at runtime
59+
},
60+
plugins: [
61+
new ModuleFederationPlugin({
62+
name: 'checkout',
63+
filename: 'remoteEntry.js',
64+
exposes: {
65+
'./CheckoutWidget': './src/CheckoutWidget',
66+
},
67+
shared: {
68+
react: { singleton: true, requiredVersion: '^18.0.0', eager: false },
69+
'react-dom': { singleton: true, requiredVersion: '^18.0.0', eager: false },
70+
},
71+
}),
72+
],
73+
};
74+
```
75+
76+
### Host (`shell`, consumes the remote)
77+
78+
```javascript
79+
// shell-app/webpack.config.js
80+
const { ModuleFederationPlugin } = require('webpack').container;
81+
82+
module.exports = {
83+
mode: 'production',
84+
plugins: [
85+
new ModuleFederationPlugin({
86+
name: 'shell',
87+
remotes: {
88+
// resolved at runtime against checkout's deployed remoteEntry.js
89+
checkout: 'checkout@https://checkout.mycdn.com/remoteEntry.js',
90+
},
91+
shared: {
92+
react: { singleton: true, requiredVersion: '^18.0.0' },
93+
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
94+
},
95+
}),
96+
],
97+
};
98+
```
99+
100+
### Loading the remote at runtime, with failure isolation
101+
102+
```tsx
103+
// shell-app/src/RemoteCheckout.tsx
104+
import { lazy, Suspense } from 'react';
105+
import { ErrorBoundary } from './ErrorBoundary';
106+
107+
// dynamic import — resolved via the federation runtime, not a static bundler dependency
108+
const CheckoutWidget = lazy(() => import('checkout/CheckoutWidget'));
109+
110+
export function RemoteCheckout(props: { orderId: string }) {
111+
return (
112+
<ErrorBoundary fallback={<CheckoutFallback orderId={props.orderId} />}>
113+
<Suspense fallback={<CheckoutSkeleton />}>
114+
<CheckoutWidget {...props} />
115+
</Suspense>
116+
</ErrorBoundary>
117+
);
118+
}
119+
```
120+
121+
### Resolving remotes dynamically instead of hardcoding URLs (common at staff level)
122+
123+
Hardcoding `checkout@https://checkout.mycdn.com/remoteEntry.js` couples the host's build to the remote's exact URL. In practice, teams resolve remote entry URLs from a runtime manifest so remotes can be versioned/rolled back independently of the host's deploy:
124+
125+
```javascript
126+
// dynamic remote registration, resolved from a manifest service at app boot
127+
async function loadRemoteEntry(manifestUrl, remoteName) {
128+
const manifest = await fetch(manifestUrl).then((r) => r.json());
129+
const remoteUrl = manifest[remoteName].entry; // e.g. served from a config/edge service
130+
131+
await __webpack_init_sharing__('default');
132+
const container = await loadScript(remoteUrl).then(() => window[remoteName]);
133+
await container.init(__webpack_share_scopes__.default);
134+
return container;
135+
}
136+
```
137+
138+
This is the piece interviewers most want to hear: **the set of remotes and their versions should be data, resolved at runtime, not baked into the host's build** — otherwise you've recreated a monolith's coupled-deploy problem with extra network hops.
139+
140+
---
141+
142+
# 3. Staff-Level Concerns (this is where the interview is actually won)
143+
144+
### Shared dependency versioning
145+
146+
`shared: { react: { singleton: true } }` means all federated apps agree to use _one_ copy of React at runtime. If a remote requires a React version incompatible with the host's, Webpack either warns and duplicates the dependency (bundle bloat, and risk of two React instances holding different Fiber trees — hooks break across the boundary) or errors, depending on `strictVersion`. A staff engineer owns a **cross-team dependency upgrade policy**: who upgrades React first, how remotes signal minimum-compatible versions, and what CI check prevents a remote from silently going out of the shared-version range.
147+
148+
### Independent deploy ≠ independent contract
149+
150+
Teams deploy independently, but the **exposed module's prop/API contract** is now a cross-team interface, same as any API contract. Treat `exposes` entries like a public API: version them, avoid breaking prop shape changes without coordination, and consider contract tests (e.g., a lightweight integration test in CI that boots the host against the remote's latest `remoteEntry.js` from a staging CDN) so a remote team can't silently break the host in production.
151+
152+
### Runtime failure isolation
153+
154+
A remote is fetched over the network at runtime — it can 404, time out, or throw during evaluation. This must **never** take down the host shell. Always wrap remote loads in an error boundary + fallback UI + timeout, and treat a failed remote load as a normal, monitored, expected failure mode (alerting on remote-load failure rate), not an exceptional crash.
155+
156+
### Performance / network waterfall
157+
158+
Federation adds a runtime fetch (`remoteEntry.js`) plus whatever chunks it lazily requests — this is an extra network round trip the monolith didn't have. Staff-level mitigations: preload critical remotes' `remoteEntry.js` via `<link rel="modulepreload">`/prefetch hints when you know a remote will be needed soon, put remoteEntry behind a CDN with long cache + content-hashed filenames, and measure whether the org's real bottleneck (bundle size vs. team velocity) actually justifies the added latency.
159+
160+
### Debuggability
161+
162+
Errors from federated code cross build boundaries — source maps, error monitoring (Sentry, etc.) need to correctly attribute a runtime error in a lazily-loaded remote back to that team's repo/release, not the shell's. Decide this **before** rollout, not after the first cross-team incident where nobody can tell whose deploy caused the regression.
163+
164+
### When NOT to use it
165+
166+
- Single team, single deploy cadence — you're paying the shared-dependency/runtime-coupling tax for no organizational benefit.
167+
- A design system / shared UI kit is usually better distributed as a **versioned npm package**, not a federated remote — federation is for independently-deployed _applications/features_, not for sharing a button component.
168+
- Small products where full-monolith rebuild+deploy is already fast (a few minutes) — the coordination overhead of federation isn't worth it until team count and deploy-cadence mismatch actually hurts.
169+
170+
---
171+
172+
# 4. How to Answer This as a Staff Engineer
173+
174+
A strong structure for a live interview answer:
175+
176+
1. **Lead with the org problem.** "This is a solution for multiple teams needing independent deploy cadence on one product surface." Don't open with Webpack config.
177+
2. **Name the trade-off explicitly.** Deploy independence and team autonomy, traded for shared-dependency governance, cross-team runtime contracts, and a harder debugging story.
178+
3. **Show you'd guard the failure modes**, not just the happy path: error boundaries around every remote, monitored remote-load failure rate, a shared-dependency version policy, and a CDN/versioning strategy that lets remotes roll back independently of the host.
179+
4. **Say when you'd say no.** Interviewers explicitly listen for engineers who reach for the trendy pattern regardless of fit versus ones who can justify _not_ using it for a single-team product.
180+
5. **Bring an example of the two hardest real bugs this pattern produces** — a React duplicate-instance bug from a shared-dependency version mismatch, and a remote silently deployed with a breaking prop-contract change — and how you'd prevent each with tooling (CI shared-version lint, contract tests) rather than tribal knowledge.
181+
182+
---
183+
184+
# 5. Staff Engineer Interview Questions
185+
186+
### 1. What problem does Module Federation solve that code-splitting alone doesn't?
187+
188+
**Expected answer:** Code-splitting (dynamic `import()`) solves bundle size within a single build/deploy. It doesn't let two separate teams deploy independently — every chunk still comes from the same build pipeline and release train. Module Federation adds _runtime_ composition of separately-built-and-deployed bundles, so team A can ship Tuesday and team B can ship Thursday without either blocking the other or even redeploying the shell.
189+
190+
### 2. Two remotes both depend on React but at different minor versions. What actually happens, and how do you prevent it from becoming an incident?
191+
192+
**Expected answer:** With `singleton: true` and a `requiredVersion` range, Webpack tries to satisfy all consumers from one shared instance if versions are semver-compatible; if not, depending on `strictVersion`, it either logs a runtime warning and loads a second copy of React (risk: hooks/context break across the boundary since two React instances don't share fiber state) or throws. Prevention is process, not code: a shared-dependency version policy enforced in CI (e.g., a lint step that fails a remote's build if its React version falls outside the org's agreed range), and a dashboard of which remotes are on which shared-dep versions so drift is visible before it causes a production bug.
193+
194+
### 3. A remote's `remoteEntry.js` fails to load in production. What's your incident response, and what should have been built in advance?
195+
196+
**Expected answer:** Immediate: the host should already degrade gracefully — error boundary shows a fallback UI, the rest of the shell keeps working, and this is an alerted, dashboarded failure mode, not a full outage. Root-causing: check whether it's a CDN/network issue, a bad deploy of the remote, or a version-manifest pointing at a URL that no longer exists. What should exist beforehand: monitoring on remote-load success rate per remote, a fast rollback path for the remote's manifest entry (point back at the last-known-good `remoteEntry.js` URL), and a runbook so any on-call engineer — not just the remote's own team — can execute that rollback.
197+
198+
### 4. How do you keep a remote team from unknowingly breaking the host with a prop-contract change?
199+
200+
**Expected answer:** Treat `exposes` modules as a versioned public API between teams, not an implementation detail. Practical mechanisms: TypeScript types for the exposed module's props published/shared between host and remote (a shared `@org/checkout-contract` types package, or generated from the remote's source), a contract/integration test in CI that mounts the host against the remote's actual latest build in staging, and a deprecation process for breaking changes (parallel old/new exposed versions during migration) rather than a hard cutover.
201+
202+
### 5. When would you tell a team _not_ to adopt micro-frontends / Module Federation?
203+
204+
**Expected answer:** Single team owning the whole surface with one deploy cadence — there's no organizational problem being solved, only added runtime complexity. Also: when the actual shared unit is a UI component library rather than an independently-releasable feature — that belongs in a versioned npm package. Also: early-stage products where team count is still small and monolith rebuild time is already fast — the coordination tax of federation (shared-version governance, cross-team contracts, harder debugging) isn't worth paying until the org actually has multiple teams whose deploy cadences are being blocked by each other.
205+
206+
### 6. How does this affect performance, and how would you measure whether it's a net win?
207+
208+
**Expected answer:** Federation adds at least one extra runtime network fetch (the remote's entry file, then its lazily-loaded chunks) compared to a single monolith bundle — this is a real latency cost, mitigated with CDN caching, content-hashed long-cache filenames, and preloading remotes you know will be needed. To decide if it's a net win, measure it against what it's bought: deploy frequency per team before/after, lead time from code-complete to production, and cross-team blocking incidents — not just raw page-load metrics, since the whole point of the pattern is organizational throughput, and a small latency regression can be worth it if deploy velocity multiplies across many teams.
209+
210+
### 7. How do you debug a production error that only reproduces when a specific combination of remote versions are live together?
211+
212+
**Expected answer:** This is the hardest class of bug federation introduces — errors that don't exist in either team's isolated staging environment because they only manifest from a specific _combination_ of independently-deployed versions. Staff-level mitigation is preventing this class rather than just debugging it after the fact: a staging/integration environment that always runs the _current production_ versions of every remote together (not each team's own latest-in-progress build), so cross-remote incompatibilities surface before prod, plus enough logging/monitoring (remote version tags attached to error reports) that when it does happen, you can immediately see which version combination was live at the time.

‎website/docs/frontend-system-design/.sync-manifest.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@
176176
"hash": "11accc02673b562dd0f4a631816ba77ec7f3a38749edfc3df74e7066c0aaf36c",
177177
"docRelative": "core/llm-text-vs-image.mdx"
178178
},
179+
"frontend-system-design/core/module-federation.mdx": {
180+
"hash": "73cb5bfcbb8d06282b4e9322b75ada9d80478459701512a193fcfbe5828a7625",
181+
"docRelative": "core/module-federation.mdx"
182+
},
179183
"frontend-system-design/core/react-performance.mdx": {
180184
"hash": "25cc7a0bbbd0c09c4d8cf7c70e97e6fbb9e37d13cd89e0c27bd14c7c1a05c95f",
181185
"docRelative": "core/react-performance.mdx"
@@ -273,7 +277,7 @@
273277
"docRelative": "video-netflix.mdx"
274278
},
275279
"__nav__:core": {
276-
"hash": "3e4e70025e2320966a442652f5c62ed53273850d655c8de3f27ca1047f4ae7a9",
280+
"hash": "e603cdb5d93b4a79b94ce18ee52a4d81e3c2b40e4f9f5dae15ba2551b6ce41ae",
277281
"docRelative": "core/_index.mdx"
278282
}
279283
}

‎website/docs/frontend-system-design/core/_index.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ title: Core
1616
- [javascript-fundamentals-interview-notes](./javascript-fundamentals-interview-notes)
1717
- [large-scale-node-link-visualization-design](./large-scale-node-link-visualization-design)
1818
- [llm-text-vs-image](./llm-text-vs-image)
19+
- [module-federation](./module-federation)
1920
- [react-performance](./react-performance)
2021
- [react-vs-web-component](./react-vs-web-component)
2122
- [shared-component-principle](./shared-component-principle)

0 commit comments

Comments
 (0)