┌──────────────────────────────────────────────────┐
│ Monolithic SPA │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Product │ │ Cart │ │
│ │ Module │ │ Catalog │ │ Module │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Search │ │ Profile │ │ Orders │ │
│ │ Module │ │ Module │ │ Module │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Single Build → Single Bundle → Deploy │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Monolithic SPA │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Product │ │ Cart │ │
│ │ Module │ │ Catalog │ │ Module │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Search │ │ Profile │ │ Orders │ │
│ │ Module │ │ Module │ │ Module │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Single Build → Single Bundle → Deploy │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ Monolithic SPA │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Product │ │ Cart │ │
│ │ Module │ │ Catalog │ │ Module │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Search │ │ Profile │ │ Orders │ │
│ │ Module │ │ Module │ │ Module │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Single Build → Single Bundle → Deploy │
└──────────────────────────────────────────────────┘
Developer A (Auth) ──┐
Developer B (Cart) ──┼──> Single Repo ──> CI Pipeline
Developer C (Search)──┘ (merge to main) │ ▼ Lint + Test (ALL code) │ ▼ Build (Entire App) │ ▼ Deploy (All or Nothing)
Developer A (Auth) ──┐
Developer B (Cart) ──┼──> Single Repo ──> CI Pipeline
Developer C (Search)──┘ (merge to main) │ ▼ Lint + Test (ALL code) │ ▼ Build (Entire App) │ ▼ Deploy (All or Nothing)
Developer A (Auth) ──┐
Developer B (Cart) ──┼──> Single Repo ──> CI Pipeline
Developer C (Search)──┘ (merge to main) │ ▼ Lint + Test (ALL code) │ ▼ Build (Entire App) │ ▼ Deploy (All or Nothing)
App grows → Build times spike (5–15 min+) → Merge conflicts multiply → One bug blocks entire deploy → Teams step on each other's code → Testing surface explodes
App grows → Build times spike (5–15 min+) → Merge conflicts multiply → One bug blocks entire deploy → Teams step on each other's code → Testing surface explodes
App grows → Build times spike (5–15 min+) → Merge conflicts multiply → One bug blocks entire deploy → Teams step on each other's code → Testing surface explodes
Year 1: Clean modules, fast builds, small team
Year 2: "Just import that util from the other module"
Year 3: "We need a shared context for user data"
Year 4: "Changing the cart broke the search page somehow"
Year 5: Fear-driven development, massive test suites, 3-week releases
Year 1: Clean modules, fast builds, small team
Year 2: "Just import that util from the other module"
Year 3: "We need a shared context for user data"
Year 4: "Changing the cart broke the search page somehow"
Year 5: Fear-driven development, massive test suites, 3-week releases
Year 1: Clean modules, fast builds, small team
Year 2: "Just import that util from the other module"
Year 3: "We need a shared context for user data"
Year 4: "Changing the cart broke the search page somehow"
Year 5: Fear-driven development, massive test suites, 3-week releases
❌ WRONG: Horizontal Layers ✅ CORRECT: Vertical Slices ┌──────────────────────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ UI Team │ │Search│ │Produc│ │ Cart │ ├──────────────────────┤ │ Team │ │ Team │ │ Team │ │ API Team │ │ │ │ │ │ │ ├──────────────────────┤ │ UI │ │ UI │ │ UI │ │ Data Team │ │ API │ │ API │ │ API │ └──────────────────────┘ │ DB │ │ DB │ │ DB │ └──────┘ └──────┘ └──────┘ Teams organized by layer Teams own full features → lots of cross-team work → autonomous delivery
❌ WRONG: Horizontal Layers ✅ CORRECT: Vertical Slices ┌──────────────────────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ UI Team │ │Search│ │Produc│ │ Cart │ ├──────────────────────┤ │ Team │ │ Team │ │ Team │ │ API Team │ │ │ │ │ │ │ ├──────────────────────┤ │ UI │ │ UI │ │ UI │ │ Data Team │ │ API │ │ API │ │ API │ └──────────────────────┘ │ DB │ │ DB │ │ DB │ └──────┘ └──────┘ └──────┘ Teams organized by layer Teams own full features → lots of cross-team work → autonomous delivery
❌ WRONG: Horizontal Layers ✅ CORRECT: Vertical Slices ┌──────────────────────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ UI Team │ │Search│ │Produc│ │ Cart │ ├──────────────────────┤ │ Team │ │ Team │ │ Team │ │ API Team │ │ │ │ │ │ │ ├──────────────────────┤ │ UI │ │ UI │ │ UI │ │ Data Team │ │ API │ │ API │ │ API │ └──────────────────────┘ │ DB │ │ DB │ │ DB │ └──────┘ └──────┘ └──────┘ Teams organized by layer Teams own full features → lots of cross-team work → autonomous delivery
┌─────────────────────────────┐ │ App Shell / Host │ │ (Routing, Layout, Auth) │ └─────────┬───────────────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌────────▼───────┐ ┌────────▼──────┐ ┌─────────▼─────┐ │ MFE: Search │ │ MFE: Product │ │ MFE: Cart │ │ (Team Alpha) │ │ (Team Beta) │ │ (Team Gamma) │ │ React 18 │ │ React 18 │ │ Vue 3 │ └────────────────┘ └───────────────┘ └───────────────┘ │ │ │ ▼ ▼ ▼ Independent CI/CD Independent CI/CD Independent CI/CD
┌─────────────────────────────┐ │ App Shell / Host │ │ (Routing, Layout, Auth) │ └─────────┬───────────────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌────────▼───────┐ ┌────────▼──────┐ ┌─────────▼─────┐ │ MFE: Search │ │ MFE: Product │ │ MFE: Cart │ │ (Team Alpha) │ │ (Team Beta) │ │ (Team Gamma) │ │ React 18 │ │ React 18 │ │ Vue 3 │ └────────────────┘ └───────────────┘ └───────────────┘ │ │ │ ▼ ▼ ▼ Independent CI/CD Independent CI/CD Independent CI/CD
┌─────────────────────────────┐ │ App Shell / Host │ │ (Routing, Layout, Auth) │ └─────────┬───────────────────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ┌────────▼───────┐ ┌────────▼──────┐ ┌─────────▼─────┐ │ MFE: Search │ │ MFE: Product │ │ MFE: Cart │ │ (Team Alpha) │ │ (Team Beta) │ │ (Team Gamma) │ │ React 18 │ │ React 18 │ │ Vue 3 │ └────────────────┘ └───────────────┘ └───────────────┘ │ │ │ ▼ ▼ ▼ Independent CI/CD Independent CI/CD Independent CI/CD
Pain / Overhead │ MFE │ / Monolith Pain Overhead │ / ------ │ / │ / ─────────┼────/──────── ← Crossover point (4-6 teams, 100K+ LoC) │ / │ / MFE Cost │ ──────────── MFE Overhead (relatively flat) │ └────────────────────────> 1 team 4-6 teams 10+ teams
Pain / Overhead │ MFE │ / Monolith Pain Overhead │ / ------ │ / │ / ─────────┼────/──────── ← Crossover point (4-6 teams, 100K+ LoC) │ / │ / MFE Cost │ ──────────── MFE Overhead (relatively flat) │ └────────────────────────> 1 team 4-6 teams 10+ teams
Pain / Overhead │ MFE │ / Monolith Pain Overhead │ / ------ │ / │ / ─────────┼────/──────── ← Crossover point (4-6 teams, 100K+ LoC) │ / │ / MFE Cost │ ──────────── MFE Overhead (relatively flat) │ └────────────────────────> 1 team 4-6 teams 10+ teams
Phase 1: Monolith serves everything ┌────────────────────────────────┐ │ MONOLITH (Search|Product|Cart)│ └────────────────────────────────┘ Phase 2: Add App Shell, extract first MFE ┌────────────────────────────────┐ │ APP SHELL │ │ /search → Search MFE (new) │ │ /* else → Monolith (legacy) │ └────────────────────────────────┘ Phase 3: Extract more MFEs over months ┌────────────────────────────────┐ │ APP SHELL │ │ /search → Search MFE │ │ /product → Product MFE │ │ /cart → Cart MFE │ │ /* else → Monolith (shrink) │ └────────────────────────────────┘ Phase 4: Monolith fully decomposed ┌────────────────────────────────┐ │ APP SHELL │ │ All routes → dedicated MFEs │ │ Monolith is gone ✓ │ └────────────────────────────────┘
Phase 1: Monolith serves everything ┌────────────────────────────────┐ │ MONOLITH (Search|Product|Cart)│ └────────────────────────────────┘ Phase 2: Add App Shell, extract first MFE ┌────────────────────────────────┐ │ APP SHELL │ │ /search → Search MFE (new) │ │ /* else → Monolith (legacy) │ └────────────────────────────────┘ Phase 3: Extract more MFEs over months ┌────────────────────────────────┐ │ APP SHELL │ │ /search → Search MFE │ │ /product → Product MFE │ │ /cart → Cart MFE │ │ /* else → Monolith (shrink) │ └────────────────────────────────┘ Phase 4: Monolith fully decomposed ┌────────────────────────────────┐ │ APP SHELL │ │ All routes → dedicated MFEs │ │ Monolith is gone ✓ │ └────────────────────────────────┘
Phase 1: Monolith serves everything ┌────────────────────────────────┐ │ MONOLITH (Search|Product|Cart)│ └────────────────────────────────┘ Phase 2: Add App Shell, extract first MFE ┌────────────────────────────────┐ │ APP SHELL │ │ /search → Search MFE (new) │ │ /* else → Monolith (legacy) │ └────────────────────────────────┘ Phase 3: Extract more MFEs over months ┌────────────────────────────────┐ │ APP SHELL │ │ /search → Search MFE │ │ /product → Product MFE │ │ /cart → Cart MFE │ │ /* else → Monolith (shrink) │ └────────────────────────────────┘ Phase 4: Monolith fully decomposed ┌────────────────────────────────┐ │ APP SHELL │ │ All routes → dedicated MFEs │ │ Monolith is gone ✓ │ └────────────────────────────────┘
Build-Time Runtime
(tight coupling, (loose coupling, best performance) more flexibility) │ │ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐
│ npm │ │ Module │ │ JS │ │ Web │ │ iframes │
│ packages│ │ Federat-│ │ Dynamic │ │ Compo- │ │ │
│ │ │ ion │ │ Remotes │ │ nents │ │ │
└─────────┘ └──────────┘ └──────────┘ └─────────┘ └─────────┘ Less autonomy More autonomy Better perf More isolation Simpler setup More complexity
Build-Time Runtime
(tight coupling, (loose coupling, best performance) more flexibility) │ │ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐
│ npm │ │ Module │ │ JS │ │ Web │ │ iframes │
│ packages│ │ Federat-│ │ Dynamic │ │ Compo- │ │ │
│ │ │ ion │ │ Remotes │ │ nents │ │ │
└─────────┘ └──────────┘ └──────────┘ └─────────┘ └─────────┘ Less autonomy More autonomy Better perf More isolation Simpler setup More complexity
Build-Time Runtime
(tight coupling, (loose coupling, best performance) more flexibility) │ │ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐
│ npm │ │ Module │ │ JS │ │ Web │ │ iframes │
│ packages│ │ Federat-│ │ Dynamic │ │ Compo- │ │ │
│ │ │ ion │ │ Remotes │ │ nents │ │ │
└─────────┘ └──────────┘ └──────────┘ └─────────┘ └─────────┘ Less autonomy More autonomy Better perf More isolation Simpler setup More complexity
Host App Shell │ ├── Fetch manifest.json → { search: "cdn/.../search.js" } │ ├── <script src="search.js"> │ └── window.searchMFE.mount(#search-root) │ └── <script src="cart.js"> └── window.cartMFE.mount(#cart-root)
Host App Shell │ ├── Fetch manifest.json → { search: "cdn/.../search.js" } │ ├── <script src="search.js"> │ └── window.searchMFE.mount(#search-root) │ └── <script src="cart.js"> └── window.cartMFE.mount(#cart-root)
Host App Shell │ ├── Fetch manifest.json → { search: "cdn/.../search.js" } │ ├── <script src="search.js"> │ └── window.searchMFE.mount(#search-root) │ └── <script src="cart.js"> └── window.cartMFE.mount(#cart-root)
User navigates to /search: Host → fetch search.js → call mount(#search-root) → Search MFE renders User navigates to /cart: Host → call search cleanup() → fetch cart.js → call mount(#cart-root)
User navigates to /search: Host → fetch search.js → call mount(#search-root) → Search MFE renders User navigates to /cart: Host → call search cleanup() → fetch cart.js → call mount(#cart-root)
User navigates to /search: Host → fetch search.js → call mount(#search-root) → Search MFE renders User navigates to /cart: Host → call search cleanup() → fetch cart.js → call mount(#cart-root)
Browser: GET /product/laptop-123 │ ▼ Composition Server (Edge) │ │ │ ▼ ▼ ▼ Header SSR Product SSR Footer SSR (50ms) (120ms) (30ms) │ │ │ └─────────┬─────────┘ ▼ Stitched HTML (130ms total) → Browser
Browser: GET /product/laptop-123 │ ▼ Composition Server (Edge) │ │ │ ▼ ▼ ▼ Header SSR Product SSR Footer SSR (50ms) (120ms) (30ms) │ │ │ └─────────┬─────────┘ ▼ Stitched HTML (130ms total) → Browser
Browser: GET /product/laptop-123 │ ▼ Composition Server (Edge) │ │ │ ▼ ▼ ▼ Header SSR Product SSR Footer SSR (50ms) (120ms) (30ms) │ │ │ └─────────┬─────────┘ ▼ Stitched HTML (130ms total) → Browser
┌────────────────────────────────────────────────────────┐
│ App Shell (Host) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Header / Navigation Bar │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌─────────────────────────────────┐ │
│ │ │ │ MFE Content Area │ │
│ │ Sidebar │ │ │ │
│ │ (shared) │ │ /search → Search MFE │ │
│ │ │ │ /product → Product MFE │ │
│ │ │ │ /cart → Cart MFE │ │
│ │ │ │ │ │
│ └──────────┘ └─────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Footer │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ App Shell (Host) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Header / Navigation Bar │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌─────────────────────────────────┐ │
│ │ │ │ MFE Content Area │ │
│ │ Sidebar │ │ │ │
│ │ (shared) │ │ /search → Search MFE │ │
│ │ │ │ /product → Product MFE │ │
│ │ │ │ /cart → Cart MFE │ │
│ │ │ │ │ │
│ └──────────┘ └─────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Footer │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ App Shell (Host) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Header / Navigation Bar │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌─────────────────────────────────┐ │
│ │ │ │ MFE Content Area │ │
│ │ Sidebar │ │ │ │
│ │ (shared) │ │ /search → Search MFE │ │
│ │ │ │ /product → Product MFE │ │
│ │ │ │ /cart → Cart MFE │ │
│ │ │ │ │ │
│ └──────────┘ └─────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Footer │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
Hardcoded (bad): Host config → "searchApp@cdn/search/v2.3.1/remoteEntry.js" Search deploys v2.4.0 → Host still points to v2.3.1! Must rebuild + redeploy host ❌ Manifest (good): Host fetches "/mfe-manifest.json" → { search: "cdn/search/v2.4.0/..." } Search deploys v2.4.0 → Updates manifest only Host automatically picks up new version ✓
Hardcoded (bad): Host config → "searchApp@cdn/search/v2.3.1/remoteEntry.js" Search deploys v2.4.0 → Host still points to v2.3.1! Must rebuild + redeploy host ❌ Manifest (good): Host fetches "/mfe-manifest.json" → { search: "cdn/search/v2.4.0/..." } Search deploys v2.4.0 → Updates manifest only Host automatically picks up new version ✓
Hardcoded (bad): Host config → "searchApp@cdn/search/v2.3.1/remoteEntry.js" Search deploys v2.4.0 → Host still points to v2.3.1! Must rebuild + redeploy host ❌ Manifest (good): Host fetches "/mfe-manifest.json" → { search: "cdn/search/v2.4.0/..." } Search deploys v2.4.0 → Updates manifest only Host automatically picks up new version ✓
✘ NEVER: import { addToCart } from '../cart-mfe/utils'; (creates build-time dependency) ✘ NEVER: window.cartMFE.state.items.push(newItem); (reaches into another MFE's internal state) ✓ GOOD: eventBus.emit('cart:add-item', { productId, qty }); (loose coupling via agreed contract) ✓ GOOD: <CartMFE items={cartItems} onCheckout={handleCheckout} /> (props from host — explicit, typed, traceable) ✓ GOOD: URL: /product/123?addedToCart=true (natural, shareable, bookmarkable state)
✘ NEVER: import { addToCart } from '../cart-mfe/utils'; (creates build-time dependency) ✘ NEVER: window.cartMFE.state.items.push(newItem); (reaches into another MFE's internal state) ✓ GOOD: eventBus.emit('cart:add-item', { productId, qty }); (loose coupling via agreed contract) ✓ GOOD: <CartMFE items={cartItems} onCheckout={handleCheckout} /> (props from host — explicit, typed, traceable) ✓ GOOD: URL: /product/123?addedToCart=true (natural, shareable, bookmarkable state)
✘ NEVER: import { addToCart } from '../cart-mfe/utils'; (creates build-time dependency) ✘ NEVER: window.cartMFE.state.items.push(newItem); (reaches into another MFE's internal state) ✓ GOOD: eventBus.emit('cart:add-item', { productId, qty }); (loose coupling via agreed contract) ✓ GOOD: <CartMFE items={cartItems} onCheckout={handleCheckout} /> (props from host — explicit, typed, traceable) ✓ GOOD: URL: /product/123?addedToCart=true (natural, shareable, bookmarkable state)
Without types: Product emits { productId: '123', qty: 1 } Cart expects { quantity: 1 } ← silent bug in prod! With types: MFEEvents['cart:add-item'] = { productId: string; quantity: number } Product tries to emit { qty: 1 } → TypeScript error ✅
Without types: Product emits { productId: '123', qty: 1 } Cart expects { quantity: 1 } ← silent bug in prod! With types: MFEEvents['cart:add-item'] = { productId: string; quantity: number } Product tries to emit { qty: 1 } → TypeScript error ✅
Without types: Product emits { productId: '123', qty: 1 } Cart expects { quantity: 1 } ← silent bug in prod! With types: MFEEvents['cart:add-item'] = { productId: string; quantity: number } Product tries to emit { qty: 1 } → TypeScript error ✅
┌────────────────────────────────────────────────────────┐
│ LAYER 1: Props from Host (top-down) │
│ Auth token, user profile, theme, feature flags │
│ Strongest contract — compile-time enforced │
├────────────────────────────────────────────────────────┤
│ LAYER 2: Typed Event Bus (peer-to-peer) │
│ 'cart:add-item', 'cart:updated', 'search:filter-change'│
│ Medium-strength — typed but runtime-enforced │
├────────────────────────────────────────────────────────┤
│ LAYER 3: URL State (shared, bookmarkable) │
│ ?q=laptop&sort=price&page=2 │
│ Implicit contract — survives refresh, shareable │
├────────────────────────────────────────────────────────┤
│ LAYER 4: Shared Backend API (eventually consistent) │
│ Cart count, user preferences, order history │
│ Loosest contract — persistent data, latency trade-off │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ LAYER 1: Props from Host (top-down) │
│ Auth token, user profile, theme, feature flags │
│ Strongest contract — compile-time enforced │
├────────────────────────────────────────────────────────┤
│ LAYER 2: Typed Event Bus (peer-to-peer) │
│ 'cart:add-item', 'cart:updated', 'search:filter-change'│
│ Medium-strength — typed but runtime-enforced │
├────────────────────────────────────────────────────────┤
│ LAYER 3: URL State (shared, bookmarkable) │
│ ?q=laptop&sort=price&page=2 │
│ Implicit contract — survives refresh, shareable │
├────────────────────────────────────────────────────────┤
│ LAYER 4: Shared Backend API (eventually consistent) │
│ Cart count, user preferences, order history │
│ Loosest contract — persistent data, latency trade-off │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ LAYER 1: Props from Host (top-down) │
│ Auth token, user profile, theme, feature flags │
│ Strongest contract — compile-time enforced │
├────────────────────────────────────────────────────────┤
│ LAYER 2: Typed Event Bus (peer-to-peer) │
│ 'cart:add-item', 'cart:updated', 'search:filter-change'│
│ Medium-strength — typed but runtime-enforced │
├────────────────────────────────────────────────────────┤
│ LAYER 3: URL State (shared, bookmarkable) │
│ ?q=laptop&sort=price&page=2 │
│ Implicit contract — survives refresh, shareable │
├────────────────────────────────────────────────────────┤
│ LAYER 4: Shared Backend API (eventually consistent) │
│ Cart count, user preferences, order history │
│ Loosest contract — persistent data, latency trade-off │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ Module Federation Runtime Flow │
├────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Browser loads Host's index.html + main.js │
│ │ │
│ ▼ │
│ Step 2: main.js calls import('./bootstrap') │
│ This creates the ASYNC BOUNDARY │
│ │ │
│ ▼ │
│ Step 3: Webpack initializes the sharing scope │
│ Registers Host's versions: React 18.2 │
│ │ │
│ ▼ │
│ Step 4: User navigates to /search │
│ lazy(() => import('searchApp/SearchPage')) │
│ │ │
│ ▼ │
│ Step 5: Webpack fetches remoteEntry.js (~5KB) from CDN │
│ │ │
│ ▼ │
│ Step 6: SHARING NEGOTIATION: │
│ Remote: "I need React ^18.0.0" │
│ Host: "I have React 18.2.0" │
│ 18.2 satisfies ^18.0 → Remote reuses Host's React │
│ │ │
│ ▼ │
│ Step 7: container.get('./SearchPage') fetches only │
│ the SearchPage chunk (~50KB), NOT entire bundle │
│ │ │
│ ▼ │
│ Step 8: SearchPage renders as a normal React component │
│ Shared React → hooks, context all work correctly │
│ │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ Module Federation Runtime Flow │
├────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Browser loads Host's index.html + main.js │
│ │ │
│ ▼ │
│ Step 2: main.js calls import('./bootstrap') │
│ This creates the ASYNC BOUNDARY │
│ │ │
│ ▼ │
│ Step 3: Webpack initializes the sharing scope │
│ Registers Host's versions: React 18.2 │
│ │ │
│ ▼ │
│ Step 4: User navigates to /search │
│ lazy(() => import('searchApp/SearchPage')) │
│ │ │
│ ▼ │
│ Step 5: Webpack fetches remoteEntry.js (~5KB) from CDN │
│ │ │
│ ▼ │
│ Step 6: SHARING NEGOTIATION: │
│ Remote: "I need React ^18.0.0" │
│ Host: "I have React 18.2.0" │
│ 18.2 satisfies ^18.0 → Remote reuses Host's React │
│ │ │
│ ▼ │
│ Step 7: container.get('./SearchPage') fetches only │
│ the SearchPage chunk (~50KB), NOT entire bundle │
│ │ │
│ ▼ │
│ Step 8: SearchPage renders as a normal React component │
│ Shared React → hooks, context all work correctly │
│ │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ Module Federation Runtime Flow │
├────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Browser loads Host's index.html + main.js │
│ │ │
│ ▼ │
│ Step 2: main.js calls import('./bootstrap') │
│ This creates the ASYNC BOUNDARY │
│ │ │
│ ▼ │
│ Step 3: Webpack initializes the sharing scope │
│ Registers Host's versions: React 18.2 │
│ │ │
│ ▼ │
│ Step 4: User navigates to /search │
│ lazy(() => import('searchApp/SearchPage')) │
│ │ │
│ ▼ │
│ Step 5: Webpack fetches remoteEntry.js (~5KB) from CDN │
│ │ │
│ ▼ │
│ Step 6: SHARING NEGOTIATION: │
│ Remote: "I need React ^18.0.0" │
│ Host: "I have React 18.2.0" │
│ 18.2 satisfies ^18.0 → Remote reuses Host's React │
│ │ │
│ ▼ │
│ Step 7: container.get('./SearchPage') fetches only │
│ the SearchPage chunk (~50KB), NOT entire bundle │
│ │ │
│ ▼ │
│ Step 8: SearchPage renders as a normal React component │
│ Shared React → hooks, context all work correctly │
│ │
└────────────────────────────────────────────────────────────┘
Build Time:
┌───────────────────────┐ ┌────────────────────────┐
│ Host App │ │ Remote: Search MFE │
│ │ │ │
│ webpack.config.js: │ │ webpack.config.js: │
│ remotes: { │ │ name: 'searchApp' │
│ searchApp: '...' │ │ exposes: { │
│ } │ │ './SearchPage' │
│ shared: ['react'] │ │ } │
│ │ │ shared: ['react'] │
└───────────────────────┘ └────────────────────────┘ Runtime:
┌───────────────────────┐ ┌────────────────────────┐
│ Host App (browser) │ │ CDN │
│ │ │ │
│ 1. User visits /search│ │ searchApp/ │
│ 2. Loads remoteEntry │─────>│ remoteEntry.js │
│ 3. Negotiates shared │ │ src_SearchPage.js │
│ deps (React) │ │ │
│ 4. Downloads only the │<─────│ │
│ SearchPage chunk │ │ │
│ 5. Renders SearchPage │ │ │
└───────────────────────┘ └────────────────────────┘
Build Time:
┌───────────────────────┐ ┌────────────────────────┐
│ Host App │ │ Remote: Search MFE │
│ │ │ │
│ webpack.config.js: │ │ webpack.config.js: │
│ remotes: { │ │ name: 'searchApp' │
│ searchApp: '...' │ │ exposes: { │
│ } │ │ './SearchPage' │
│ shared: ['react'] │ │ } │
│ │ │ shared: ['react'] │
└───────────────────────┘ └────────────────────────┘ Runtime:
┌───────────────────────┐ ┌────────────────────────┐
│ Host App (browser) │ │ CDN │
│ │ │ │
│ 1. User visits /search│ │ searchApp/ │
│ 2. Loads remoteEntry │─────>│ remoteEntry.js │
│ 3. Negotiates shared │ │ src_SearchPage.js │
│ deps (React) │ │ │
│ 4. Downloads only the │<─────│ │
│ SearchPage chunk │ │ │
│ 5. Renders SearchPage │ │ │
└───────────────────────┘ └────────────────────────┘
Build Time:
┌───────────────────────┐ ┌────────────────────────┐
│ Host App │ │ Remote: Search MFE │
│ │ │ │
│ webpack.config.js: │ │ webpack.config.js: │
│ remotes: { │ │ name: 'searchApp' │
│ searchApp: '...' │ │ exposes: { │
│ } │ │ './SearchPage' │
│ shared: ['react'] │ │ } │
│ │ │ shared: ['react'] │
└───────────────────────┘ └────────────────────────┘ Runtime:
┌───────────────────────┐ ┌────────────────────────┐
│ Host App (browser) │ │ CDN │
│ │ │ │
│ 1. User visits /search│ │ searchApp/ │
│ 2. Loads remoteEntry │─────>│ remoteEntry.js │
│ 3. Negotiates shared │ │ src_SearchPage.js │
│ deps (React) │ │ │
│ 4. Downloads only the │<─────│ │
│ SearchPage chunk │ │ │
│ 5. Renders SearchPage │ │ │
└───────────────────────┘ └────────────────────────┘
index.js → import('./bootstrap') → bootstrap.js (actual app code) │ └── This gap is where sharing negotiation happens
index.js → import('./bootstrap') → bootstrap.js (actual app code) │ └── This gap is where sharing negotiation happens
index.js → import('./bootstrap') → bootstrap.js (actual app code) │ └── This gap is where sharing negotiation happens
Host has React 18.2, Remote has React 18.3 in package.json Sharing negotiation: Remote: "I need React ^18.0.0" Host: "I have React 18.2.0" 18.2 satisfies ^18.0 → Remote uses Host's React ✅ (ONE copy) If incompatible: Remote: "I need React ^19.0.0" Host: "I have React 18.2.0" 18.2 does NOT satisfy ^19.0 → Remote loads own React ⚠️ (TWO copies)
Host has React 18.2, Remote has React 18.3 in package.json Sharing negotiation: Remote: "I need React ^18.0.0" Host: "I have React 18.2.0" 18.2 satisfies ^18.0 → Remote uses Host's React ✅ (ONE copy) If incompatible: Remote: "I need React ^19.0.0" Host: "I have React 18.2.0" 18.2 does NOT satisfy ^19.0 → Remote loads own React ⚠️ (TWO copies)
Host has React 18.2, Remote has React 18.3 in package.json Sharing negotiation: Remote: "I need React ^18.0.0" Host: "I have React 18.2.0" 18.2 satisfies ^18.0 → Remote uses Host's React ✅ (ONE copy) If incompatible: Remote: "I need React ^19.0.0" Host: "I have React 18.2.0" 18.2 does NOT satisfy ^19.0 → Remote loads own React ⚠️ (TWO copies)
Remote requests 'react': │ ├── Is singleton: true? │ ├── YES → Host has react in share scope? │ │ ├── YES → Host version satisfies requiredVersion? │ │ │ ├── YES → ✅ Use Host's version (shared!) │ │ │ └── NO → strictVersion: true? │ │ │ ├── YES → 💥 RUNTIME ERROR │ │ │ └── NO → ⚠️ Warning, use Host's anyway │ │ └── NO → Remote loads its own (bad!) │ │ │ └── NO (not singleton) → │ Version satisfied? → Share (save bandwidth) │ Not satisfied? → Load own copy (OK for stateless libs)
Remote requests 'react': │ ├── Is singleton: true? │ ├── YES → Host has react in share scope? │ │ ├── YES → Host version satisfies requiredVersion? │ │ │ ├── YES → ✅ Use Host's version (shared!) │ │ │ └── NO → strictVersion: true? │ │ │ ├── YES → 💥 RUNTIME ERROR │ │ │ └── NO → ⚠️ Warning, use Host's anyway │ │ └── NO → Remote loads its own (bad!) │ │ │ └── NO (not singleton) → │ Version satisfied? → Share (save bandwidth) │ Not satisfied? → Load own copy (OK for stateless libs)
Remote requests 'react': │ ├── Is singleton: true? │ ├── YES → Host has react in share scope? │ │ ├── YES → Host version satisfies requiredVersion? │ │ │ ├── YES → ✅ Use Host's version (shared!) │ │ │ └── NO → strictVersion: true? │ │ │ ├── YES → 💥 RUNTIME ERROR │ │ │ └── NO → ⚠️ Warning, use Host's anyway │ │ └── NO → Remote loads its own (bad!) │ │ │ └── NO (not singleton) → │ Version satisfied? → Share (save bandwidth) │ Not satisfied? → Load own copy (OK for stateless libs)
URL: /search/results?q=laptop&sort=price │ │ │ └──────────────────┐ ▼ ▼ Host Router MFE Router matches /search/* matches /results → loads Search MFE → renders SearchResults HOST RESPONSIBILITY: MFE RESPONSIBILITY: - Top-level route matching - Sub-route matching - Loading/unloading MFEs - Internal navigation - Cross-MFE navigation - Query param management
URL: /search/results?q=laptop&sort=price │ │ │ └──────────────────┐ ▼ ▼ Host Router MFE Router matches /search/* matches /results → loads Search MFE → renders SearchResults HOST RESPONSIBILITY: MFE RESPONSIBILITY: - Top-level route matching - Sub-route matching - Loading/unloading MFEs - Internal navigation - Cross-MFE navigation - Query param management
URL: /search/results?q=laptop&sort=price │ │ │ └──────────────────┐ ▼ ▼ Host Router MFE Router matches /search/* matches /results → loads Search MFE → renders SearchResults HOST RESPONSIBILITY: MFE RESPONSIBILITY: - Top-level route matching - Sub-route matching - Loading/unloading MFEs - Internal navigation - Cross-MFE navigation - Query param management
Host: /search/* → Search MFE /product/* → Product MFE /cart/* → Cart MFE Search MFE sub-routes: /search/ → SearchHome /search/results → SearchResults /search/filters → SearchFilters
Host: /search/* → Search MFE /product/* → Product MFE /cart/* → Cart MFE Search MFE sub-routes: /search/ → SearchHome /search/results → SearchResults /search/filters → SearchFilters
Host: /search/* → Search MFE /product/* → Product MFE /cart/* → Cart MFE Search MFE sub-routes: /search/ → SearchHome /search/results → SearchResults /search/filters → SearchFilters
Search MFE: push → lint → test → build → deploy to CDN → update manifest
Product MFE: push → lint → test → build → deploy to CDN → update manifest
Host: push → lint → test → build → deploy (rarely changes)
Search MFE: push → lint → test → build → deploy to CDN → update manifest
Product MFE: push → lint → test → build → deploy to CDN → update manifest
Host: push → lint → test → build → deploy (rarely changes)
Search MFE: push → lint → test → build → deploy to CDN → update manifest
Product MFE: push → lint → test → build → deploy to CDN → update manifest
Host: push → lint → test → build → deploy (rarely changes)
CDN structure:
cdn.myapp.com/search/ v2.3.0/remoteEntry.js ← Previous v2.3.1/remoteEntry.js ← Current Manifest points to current. Rollback = update manifest to previous version.
No rebuild needed. Old bundles stay on CDN.
CDN structure:
cdn.myapp.com/search/ v2.3.0/remoteEntry.js ← Previous v2.3.1/remoteEntry.js ← Current Manifest points to current. Rollback = update manifest to previous version.
No rebuild needed. Old bundles stay on CDN.
CDN structure:
cdn.myapp.com/search/ v2.3.0/remoteEntry.js ← Previous v2.3.1/remoteEntry.js ← Current Manifest points to current. Rollback = update manifest to previous version.
No rebuild needed. Old bundles stay on CDN.
Start │ ├── How many teams? │ │ │ ├── 1-3 teams → MONOLITH (with code-splitting) │ │ Lazy routes, good folder structure │ │ │ └── 4+ teams → │ │ │ ├── Clear domain boundaries? │ │ │ │ │ ├── Yes → MICRO-FRONTENDS │ │ │ │ │ │ │ ├── Need framework mixing? │ │ │ │ ├── Yes → Web Components / Single-SPA │ │ │ │ └── No → Module Federation │ │ │ │ │ │ │ ├── SEO critical? │ │ │ │ ├── Yes → Server-side composition │ │ │ │ └── No → Client-side composition │ │ │ │ │ │ │ └── Legacy migration? │ │ │ ├── Yes → iframes → migrate to MF │ │ │ └── No → Module Federation from day 1 │ │ │ │ │ └── No (highly coupled) → MONOLITH (monorepo + Nx/Turborepo) │ │ │ └── Not sure → Monolith with CLEAR MODULE BOUNDARIES │ Migrate to MFE when pain points emerge └── End
Start │ ├── How many teams? │ │ │ ├── 1-3 teams → MONOLITH (with code-splitting) │ │ Lazy routes, good folder structure │ │ │ └── 4+ teams → │ │ │ ├── Clear domain boundaries? │ │ │ │ │ ├── Yes → MICRO-FRONTENDS │ │ │ │ │ │ │ ├── Need framework mixing? │ │ │ │ ├── Yes → Web Components / Single-SPA │ │ │ │ └── No → Module Federation │ │ │ │ │ │ │ ├── SEO critical? │ │ │ │ ├── Yes → Server-side composition │ │ │ │ └── No → Client-side composition │ │ │ │ │ │ │ └── Legacy migration? │ │ │ ├── Yes → iframes → migrate to MF │ │ │ └── No → Module Federation from day 1 │ │ │ │ │ └── No (highly coupled) → MONOLITH (monorepo + Nx/Turborepo) │ │ │ └── Not sure → Monolith with CLEAR MODULE BOUNDARIES │ Migrate to MFE when pain points emerge └── End
Start │ ├── How many teams? │ │ │ ├── 1-3 teams → MONOLITH (with code-splitting) │ │ Lazy routes, good folder structure │ │ │ └── 4+ teams → │ │ │ ├── Clear domain boundaries? │ │ │ │ │ ├── Yes → MICRO-FRONTENDS │ │ │ │ │ │ │ ├── Need framework mixing? │ │ │ │ ├── Yes → Web Components / Single-SPA │ │ │ │ └── No → Module Federation │ │ │ │ │ │ │ ├── SEO critical? │ │ │ │ ├── Yes → Server-side composition │ │ │ │ └── No → Client-side composition │ │ │ │ │ │ │ └── Legacy migration? │ │ │ ├── Yes → iframes → migrate to MF │ │ │ └── No → Module Federation from day 1 │ │ │ │ │ └── No (highly coupled) → MONOLITH (monorepo + Nx/Turborepo) │ │ │ └── Not sure → Monolith with CLEAR MODULE BOUNDARIES │ Migrate to MFE when pain points emerge └── End - Monolithic Frontend Architecture
- What Are Micro Frontends?
- Monolith vs Micro Frontend Tradeoffs
- MFE Integration / Composition Approaches
- Hosting Multiple MFEs Under One UI
- Cross Communication Between MFEs
- Module Federation Deep Dive
- Module Federation 2.0
- Shared Dependencies and Versioning
- Routing in Micro Frontends
- Deployment and CI/CD
- Real World Examples
- Decision Flowchart - Small-to-medium app with 1–3 teams
- Features are tightly coupled (e.g., dashboard where every widget shares state)
- Your org doesn't need independent deployment cadences
- Early-stage products where speed of iteration matters most - Third-party widget embedding (Intercom, payment forms) — you don't control the code
- Legacy migration — wrap the old jQuery monolith in an iframe while building new MFEs
- Security-critical sections — iframe the payment page so XSS in the main app can't steal card data - Each MFE exports mount(container) and optionally unmount(), bootstrap()
- The Host decides when to call them based on routes
- Each MFE is framework-agnostic — mount can call ReactDOM.render(), createApp().mount() (Vue), or vanilla DOM
- The cleanup function is critical — without proper unmount, you get memory leaks - Coupling vs Autonomy: Build-time approaches (npm) give tight integration and best performance but sacrifice independent deployment. Runtime approaches (iframes, Module Federation) give full autonomy but add network requests and potential failure points.
- Isolation vs Shared Resources: iframes give perfect isolation but zero resource sharing. Module Federation gives shared dependencies but imperfect isolation (MFEs share the same DOM and can accidentally interfere with CSS).
- Performance vs Flexibility: Best performance comes from a single optimized bundle. Most flexibility comes from runtime code loading. No approach maximizes both simultaneously. - Routing — which MFE to load for a given URL
- Layout — shared chrome (header, sidebar, footer)
- Authentication — auth tokens/context for all MFEs
- Loading MFEs — fetching and mounting the correct MFE
- Shared Services — analytics, error tracking, feature flags - Hooks break: useState registers in one React's registry. If a component renders in another React's tree, you get "Invalid hook call" — the #1 Module Federation debugging nightmare.
- Context doesn't work: useContext across different React instances can't share context. A ThemeProvider wrapping the host's React won't provide values to a remote using a different instance.
- Events break: React's synthetic event system is per-instance. - MFEs don't have their own router — receive route info as props
- Use MemoryRouter for internal navigation (doesn't touch URL bar), delegate cross-MFE navigation to the Host via callback or event bus
- Use basename prop and the Host listens for popstate events - window.history.pushState({}, '', '/cart') + dispatch popstate event
- eventBus.emit('nav:navigate', { path: '/cart' })
- Call props.navigate('/cart') from host-provided callback - Double router bug: MFE updates URL but Host's router doesn't see it → wrong MFE renders
- Back button confusion: MemoryRouter changes don't touch browser history → back button skips MFE-internal navigation
- Deep linking failure: MFE doesn't read URL params on mount → shared URLs show default view instead of expected state - Per-MFE: Unit + integration tests in each pipeline (fast, isolated)
- Composition testing: Staging environment running latest of every MFE from manifest. Run E2E tests (Playwright) on every MFE deploy. Block manifest update if E2E fails.
- Contract testing: Verify cross-MFE event payload shapes. If Product changes cart:add-item payload, Cart's contract test fails before deployment. Use Pact or JSON Schema validation.
- Breaking changes: Deploy the consumer first (handles old + new format), then deploy the producer. Same "expand and contract" pattern as database migrations. - Monolith first. Don't adopt MFE until you feel the pain at organizational scale. MFE is an organizational architecture decision, not a technical one.
- Module Federation is the most popular approach for React-based MFEs. It solves the shared dependency problem. Use MF 2.0 for type safety and bundler flexibility (Vite, Rspack).
- App Shell is the standard hosting pattern — thin host handles routing, layout, auth. Keep it stable so it rarely redeploys.
- Cross-MFE communication should be layered: Props for auth/config, Typed Event Bus for peer-to-peer, URL for filters/navigation, Shared API for persistent data. Never import another MFE's code directly.
- Shared singletons (singleton: true for React, Redux, Router) prevent duplicate instances that break hooks, context, and events.
- Async boundary (import('./bootstrap')) is mandatory for Module Federation's sharing negotiation to work.
- Manifest + CDN enables true independent deployment, instant rollbacks, canary releases, and A/B testing without rebuilding the host.
- The right answer to "Should we use MFEs?" is: "It depends on your organization's size, structure, DevOps maturity, and whether monolith pain exceeds MFE overhead."