Tools: Frontend System Design: Micro Frontends, Monolith vs MFE

Tools: Frontend System Design: Micro Frontends, Monolith vs MFE

Micro Frontends, Module Federation and Cross Communication

Table of Contents

1. Monolithic Frontend Architecture

Build & Deploy Cycle

When Monolith Works Well

Pain Points at Scale

The Coupling Cascade

2. What Are Micro Frontends?

Vertical Slices, Not Horizontal Layers

Core Principles

Architecture Overview

3. Monolith vs Micro Frontend Tradeoffs

The Inflection Point

When to Choose MFEs

When NOT to Choose MFEs

Strangler Fig Migration Pattern

4. MFE Integration / Composition Approaches

Composition Spectrum

4.1 Build Time Integration (npm Packages)

4.2 Runtime Integration via iframes

4.3 Runtime Integration via JavaScript (Dynamic Remotes)

4.4 Runtime Integration via Web Components

4.5 Runtime Integration via Module Federation (Webpack 5)

4.6 Server-Side Composition

Comparison Matrix

Three Underlying Tensions

5. Hosting Multiple MFEs Under One UI

The App Shell Pattern

Hosting Strategies

Discovery Service / Manifest

6. Cross Communication Between MFEs

Communication Rules

6.1 Custom Events (DOM Events)

6.2 Pub/Sub Event Bus (Typed)

6.3 Props Down from Host

6.4 URL / Query Params

6.5 BroadcastChannel API

6.6 Shared Backend / APIs

Comparison

Best Practice: Layer Your Communication

7. Module Federation Deep Dive

What Is Module Federation?

Key Concepts

Runtime Flow (Step by Step)

Build Time vs Runtime

The Async Boundary (Critical!)

Dynamic Remotes

Bidirectional Federation

8. Module Federation 2.0

Key Improvements

9. Shared Dependencies and Versioning

How Sharing Works

Why Duplicate React is Catastrophic

Sharing Negotiation Algorithm

Version Strategy

Organizational Strategy

10. Routing in Micro Frontends

Routing Delegation Model

Approach: Nested Route Delegation

The Single Router Rule

Cross-MFE Navigation

Common Routing Bugs

11. Deployment and CI/CD

Independent Pipelines

Versioning & Rollback

Testing Strategy

12. Real World Examples

13. Decision Flowchart

Quick Decision Matrix

Key Takeaways A guide to Monolithic vs Micro Frontend architectures, composition strategies, cross-MFE communication patterns, and Webpack Module Federation — focused on when to use what and why. A monolithic frontend is a single, unified codebase where every page, feature, and route lives together. One team (or multiple teams in the same repo) builds, tests, and deploys the entire application as a single unit. Developer A's one-line auth fix must wait for Developer B's half-finished cart feature to be merged or reverted. Monoliths are not bad — they are the correct starting point. The problems only emerge at organizational scale, not application scale. A well-structured monolith with lazy routes and code-splitting can serve a product for years. Most of the benefits attributed to MFEs can be partially achieved within a well-structured monolith: lazy loading, monorepo tooling (Nx, Turborepo), and CODEOWNERS files. The one thing a monolith cannot give you is independent deployment. Rule of thumb: If your team can ship a feature from code review to production in under a day, your monolith is not the bottleneck. Nothing prevents cross-module coupling in a monolith. Any file can import any other file. Developers under deadline pressure take shortcuts — import a utility from another module, share a React context, add a field to a shared Redux store. Each shortcut is harmless individually, but collectively they create a web of implicit dependencies that makes independent changes impossible. Micro-frontends solve this by creating hard boundaries — separate repos, separate builds, separate deploys. You physically cannot import from another MFE's source code, forcing teams to communicate through explicit contracts. Micro-frontends extend the microservices idea to the frontend: split a large monolithic UI into independently developed, tested, deployed, and hosted frontend applications that are composed together in the browser to feel like one cohesive product. "An architectural style where independently deliverable frontend applications are composed into a greater whole." — Martin Fowler The most common mistake is thinking of MFEs as horizontal layers (UI team, API team, data team). Instead, MFEs are vertical slices — each team owns an entire feature end-to-end. Each MFE owns its own UI, state, routes, and ideally communicates with its own backend microservice. The Search team owns the search UI (React app), search API (Node/Go service), and search database (Elasticsearch). They can ship without coordinating with anyone. Technology Agnostic sounds appealing but is almost always the wrong default. Running React + Vue means double framework bundles, two tooling sets, and difficulty sharing a design system. The real value is for incremental migration — moving from Angular to React MFE by MFE. In a monolith, coordination cost scales O(n²) with team count (every team's changes can conflict with every other's). MFEs reduce this to O(n) because teams only coordinate at well-defined boundaries. ✅ Large org with 5+ autonomous teams

✅ Teams need different release cadences✅ Clear domain boundaries (e.g., search, product, cart, checkout)✅ Need to incrementally migrate a legacy monolith ❌ Small team (< 5 devs)❌ Features are heavily coupled with lots of shared state❌ Org doesn't have DevOps maturity for multiple pipelines❌ Building a prototype or MVP Most real-world MFE adoption is gradually migrating a monolith, not greenfield. The Strangler Fig pattern wraps the old system and replaces it piece by piece: At every phase the system is fully functional. If a new MFE has bugs, route it back to the monolith with a config change. Each phase delivers immediate measurable value — unlike "big bang" rewrites that deliver zero value until 100% complete. The hardest part is shared authentication and layout. The monolith and new MFEs must share the same login session (via cookies or localStorage tokens) and maintain a consistent header/footer. Build the App Shell and auth integration first. MFEs are published as npm packages and the host installs them as dependencies. The host imports and bundles them at compile time. Why this isn't true MFE: Every time the Search team ships a new version, the Host must bump the version, rebuild, run CI/CD, and deploy. The Host becomes a bottleneck. In a 10-team org, the Host could rebuild 5 times a day just to pick up others' changes. Works great for shared component libraries where you want explicit opt-in to new versions, not for independent deployment. Each MFE is loaded inside an <iframe>. Each iframe gets a separate browsing context with its own DOM, CSS engine, JS heap, and event loop. When to actually use iframes: The host loads each MFE's JS bundle at runtime. Each MFE exports a mount(container) function. This is the pattern Single-SPA popularized. Each MFE wraps itself as a Custom Element using the browser-native Web Components API. Shadow DOM provides style isolation without iframe overhead. Web Components act as a framework-agnostic wrapper: your React MFE renders inside a custom element's Shadow DOM, and from the host's perspective it's just a <mfe-search> tag. The practical challenge is passing complex data — HTML attributes are strings, so you either serialize to JSON or use JS properties. The most popular modern approach. See Section 7 for the deep dive. MFEs are composed on the server before HTML reaches the browser. A composition layer (edge function, CDN, Node.js middleware) fetches HTML fragments from each MFE's SSR service and stitches them into one page. Technologies: Podium, Piral, Tailor (Zalando), ESI (Edge Side Includes) The critical challenge is hydration coordination: each fragment was rendered by a different framework instance. Each MFE must hydrate only its own fragment with separate hydration scripts. Don't memorize the table. Understand the three tensions that drive every cell: 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. Practical recommendation: Module Federation for Webpack/Vite + React teams. Server-side composition when SEO is a hard requirement. iframes only for truly untrusted third-party content. The key challenge: how do you make 3–10 independently deployed apps feel like one product? The App Shell (Host/Container) is a lightweight application responsible for: The shell should be thin and stable — it deploys rarely while MFEs deploy frequently. Think of it like an OS kernel: small, rarely updated. If the shell had significant business logic, it would become a bottleneck. In production, the host doesn't hardcode MFE URLs. It fetches a manifest at runtime — the same pattern as service discovery in backend microservices (Consul, etcd, K8s DNS). The manifest enables canary deployments (5% of users see new version), instant rollbacks (update manifest to previous version — no rebuild), A/B testing at MFE level, and an emergency kill switch (revert within CDN cache TTL of ~60 seconds). Cache strategy: Manifest served with a short TTL (60 seconds) so updates propagate quickly. MFE bundles served with long TTLs (1 year) using content-hashed filenames since they're immutable. One of the hardest MFE problems. MFEs must be independent (no shared imports), yet the UX demands they feel connected (clicking "Add to Cart" in Product MFE should update the cart badge in Cart MFE). Uses the browser's native CustomEvent API. One MFE dispatches on document, another listens. When to use: 2–3 MFEs with a handful of fire-and-forget notifications like "user logged in" or "item added to cart". When NOT to use: 10+ event types across 5+ MFEs — you lose track of who's emitting what. A lightweight event bus shared across MFEs via a singleton on window. The key improvement over raw CustomEvents is a typed event contract — a single interface that defines all cross-MFE events and their payload shapes. The MFEEvents interface acts as a shared contract living in a shared package (@org/mfe-contracts). When one team changes a payload shape, TypeScript flags all consumers at build time instead of silently breaking in production. Best for: Most MFE architectures. The go-to pattern for peer-to-peer communication. The host passes data and callbacks to MFEs as props. Explicit, traceable, type-safe with TypeScript. Best for: Few MFEs, simple data flow. Auth context, user info, feature flags. The URL is a natural, framework-agnostic state container. Any MFE can read/write query params. It's bookmarkable, shareable, and survives page refreshes. Best for: Filter state, search queries, pagination, sort order — any state multiple MFEs need to read and that should survive refresh. Limits: String-only, publicly visible, ~2000 char limit, can't hold complex nested objects. For cross-tab communication or MFEs in separate iframes. Example: user logs out in one tab → BroadcastChannel notifies all tabs → all redirect to login. Impossible with events or event bus since those only work within a single document. MFEs don't talk to each other — they share the same backend APIs. Most loosely coupled pattern, but the trade-off is latency (Cart badge won't update instantly on "Add to Cart"). Pair with optimistic UI updates. Anti-pattern: Reaching for the Event Bus when Props would be simpler. If the host already has the data, just pass it as props. Save the event bus for genuinely peer-to-peer interactions. A Webpack 5 feature that lets an app dynamically load code from another independently built and deployed app at runtime while sharing dependencies to avoid duplication. Think of it as runtime npm. Before Module Federation (pre-2020), choices were: npm packages (loses independent deployment), iframes (terrible UX), or custom script-loading hacks (no dependency sharing). Module Federation was created to solve the shared dependency problem — multiple independent apps sharing React at runtime without duplicating it. This is the critical flow to understand — how code actually loads at runtime: Module Federation requires an async boundary at the entry point. Without it, shared dependency negotiation fails. Without the async boundary, the host's main entry executes synchronously before the sharing scope is initialized. Remote modules try to resolve react from the sharing scope, find nothing, and either crash with "Shared module is not available for eager consumption" or load their own separate copy (defeating sharing). The import('./bootstrap') creates an async chunk boundary, giving Webpack time to: (1) parse all remoteEntry.js files, (2) build the sharing scope, (3) resolve version negotiations, (4) then start rendering. Instead of hardcoding remote URLs at build time, load them from a manifest at runtime. This enables true independent deployment — the host doesn't need to rebuild when a remote deploys a new version. The flow: inject <script> for remoteEntry.js, call __webpack_init_sharing__, initialize the container, then get the specific module. Any app can be both host AND remote simultaneously. App A exposes Header/Footer while consuming ProductCatalog from App B, and vice versa. When to use: Shared layout components. The Platform team exposes Header/Footer as a remote, but their admin dashboard consumes ProductCatalog from Product MFE. Caution: Circular dependencies can cascade failures. Keep bi-directional surface area small (shared UI shells, not business logic). MF 1.0 was Webpack-specific, had no type safety, and required Webpack internal hacks for advanced use cases. MF 2.0 decouples the federation runtime from the bundler. The biggest win is type safety. In MF 1.0, import('searchApp/SearchPage') was completely untyped. MF 2.0 generates TypeScript declarations from the remote's actual exports — full autocomplete and compile-time errors. MF 2.0 uses @module-federation/enhanced as a drop-in replacement for ModuleFederationPlugin. For new projects, start with MF 2.0. For existing MF 1.0 setups, migration is straightforward. The biggest performance concern in MFEs: duplicate dependencies. It's not just bundle size — it's a runtime correctness issue. React uses module-level state for its hooks system (fiber tree, hook queue). Two React instances means: This is why singleton: true is mandatory for React, ReactDOM, and any library with module-level state. The most dangerous moment is when one team upgrades React and others haven't. Establish a shared dependency upgrade cadence — all teams upgrade React together once per quarter. The Platform team proposes the version, tests it in staging, and coordinates rollout. Set strictVersion: false (the default) as a safety net. On version mismatch, Webpack logs a warning but still uses the Host's version. Minor version differences (18.2 vs 18.3) rarely cause real issues. Only use strictVersion: true if a mismatch would cause data corruption. Each MFE has its own routes, but the user sees one URL bar. The Host owns top-level routes with wildcard /*. Each MFE owns everything under its prefix: If each MFE uses its own BrowserRouter, multiple router instances fight over the URL bar. When Search MFE calls navigate('/cart'), its own router processes it but the Host's router doesn't know — Cart MFE never loads. Option 2 is the most common in production. The MFE owns internal navigation (tabs, filters) via MemoryRouter. For "leave this MFE" navigation, it fires an event or calls the Host's navigate(). Golden rule: The Host owns the URL bar. MFEs can read the URL to initialize state, but should only write through the Host's navigation API. Each MFE has its own pipeline. The Host shell changes rarely. MFE rollback: Update manifest (one config change, ~60 seconds propagation)

Monolith rollback: Revert interleaved git commits, cherry-pick, rebuild (15 min), redeploy (30–60 min) Canary deployments: The manifest serves different versions to different user cohorts. Start at 1% traffic, monitor errors, scale to 100% if healthy. Every company below adopted MFEs because of an organizational problem, not a technology problem. The pattern: IKEA needed geographic autonomy, Spotify needed squad independence, Zalando needed SEO with team autonomy, Amazon needed thousands of engineers to not step on each other, banking needed regulatory isolation. If asked "Should we use MFEs?", the right first question is: "What organizational problem are you solving?" If the answer is "slow builds" or "large bundles", solve those within a monolith (code-splitting, CDN caching). If the answer is "teams blocking each other's deployments", that's when MFEs earn their complexity tax. 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." Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

┌──────────────────────────────────────────────────┐ │ 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."