Micro-frontends & Monorepo — Cheat Sheet

FE Lead prep · ref 14 · §2.9 §2.10 · lead with the COST, not the hype · "have a strong opinion"

The one frame to memorize

Micro-frontends = an ORGANIZATIONAL solution, not a technical one. You split the frontend to split the teams (independent deploys). Small team → monolith is 10× more efficient. They make the app slower, not faster.
orthogonal  Monorepo = build-time source layout. Micro-frontend = runtime composition. Different axes — you can have either without the other. Conflating them is the junior tell.

Why / Why-not MFE

Adopt when
Don't when
3 benefits (all about people): independent deploy · autonomous teams · incremental upgrade. “Decouple teams, not code.”

Integration spectrum (where composition happens)

ApproachNote
Build-time (npm pkg)❌ re-couples deploys — shell must rebuild to ship a remote. Defeats purpose.
Server-side compositionedge/SSR assembles fragments — best for SEO/first paint
Iframesbulletproof isolation, awful routing/state/height
Runtime JSmost common. single-spa=orchestrator · Module Federation=shares deps
Web Components / import mapsframework-neutral, native; less tooling/DX
// Module Federation: host loads remote at runtime, shares ONE React
remotes: { cart: 'cart@https://cart.example.com/remoteEntry.js' },
shared:  { react: { singleton: true, requiredVersion: '^18.2.0' } }
// singleton:true ⇒ one copy (no dup-React/hook bugs). The version
// negotiation here IS where "version skew" can crash a remote @ runtime.

The 5 costs (the graded part — name them)

Fowler's core tension: "tension between letting teams compile independently (autonomy) and sharing common dependencies." Every MFE call is a point on that line.

Two versions of one lib — how MF resolves it

ConfigOn version mismatch
defaultshares highest COMPATIBLE; incompatible → each falls back to own copy (2 versions side-by-side, dup bytes)
singleton:trueone copy, HIGHEST wins regardless of compat + console warning on mismatch. Required for React (2 copies = broken hooks)
strictVersion:truewarning → hard runtime error (fail fast)
requiredVersionwiden accepted range (e.g. '>=18 <20') to permit drift on purpose
eager:trueload in initial chunk (sync) vs async-negotiated; bloats entry — prefer lazy
No magic: incompatible majors = either duplicate (default) or one shared (singleton, skew risk / strictVersion crash). So govern shared versions (renovate + single source of truth), don't rely on negotiation. Stateful libs (React/router/store) MUST dedupe; stateless utils (lodash) duplication = size cost only.
Resolution rule: a consumer takes the highest provided version that satisfies its own caret range, else its own copy (non-singleton) / forced onto highest-at-init + warn (singleton). Floats up, never below its floor, never across a major. Gotchas: ① exact requiredVersion pins ("18.2.0") defeat sharing even within a major → use carets. ② Widening requiredVersion:'>=18 <20' = consent to a higher major → no warning. ③ Singleton resolves at first consume (React = render time) → a late remote can't upgrade the live singleton, and can be pushed below its own required version. ④ Non-singleton late remote → loads its own copy at click time (duplication paid lazily).

Remote config, deploy & caching ⚠️

Terms (don't mix): remoteEntry.js = the entry file / pointer — tiny JS at a fixed un-hashed URL that names the content-hashed chunks holding the real code (e.g. cart.8f3a9c.js). mf-manifest.json (MF 2.0) = separate JSON metadata (exposes/shared/chunks). Model: one mutable pointer → many immutable chunks.
Static remote = URL baked in host build (rebuild to change/rollback). Dynamic = URL from runtime config via init()/loadRemote('cart/api') (no host rebuild).
Deploy: remote overwrites remoteEntry.js in place at the same URL → now points at new hashed chunks. Host fetches it next load → picks up new version, no host redeploy.
The paradox / #1 outage: fixed URL + cached entry → host reads OLD pointer, never updates. Old chunks purged → asks for a 404'd chunk → ChunkLoadError / white screen. (Fresh entry → loads new chunks ✓.)
FileCache-Control
remoteEntry.js (pointer)no-cache / short-TTL / ?t= bust → always revalidate
hashed chunksimmutable, max-age=31536000 (name changes on change = safe forever)
+ Retain old chunks through deploy window · runtime registry (name→version URL): deploy=update it, rollback=repoint it (config flip) · error boundary per remote. "Entry itself cached?" → there's always one pointer you can't cache; shrink it to a 1-line registry {"cart":"v1.2.3"} (no-cache) so every remoteEntry.js becomes immutable/versioned. Trade-off: mutable "latest" (cache race / no rollback) vs versioned+registry (control + clean rollback).
"Static+no-cache works — why dynamic?" Different axis. no-cache = "always-latest of a known URL." Dynamic = decide which URL / version / whether to load at runtime → pin & rollback w/o host redeploy, per-env URLs (1 artifact), add/remove remotes w/o touching host, conditional/A-B load, atomic versioned rollout. Static+no-cache only gives auto-latest (no pin/rollback).
config.json = host-level registry (name→current versioned entry URL); edit a line = release/rollback. Lives on shell CDN / edge-KV / flag-service (platform-owned). This is THE no-cache file → everything below it (versioned remoteEntry.js + hashed chunks) is immutable. Caveats: on the critical path (inline into HTML / edge-cache) + single source of truth (needs fallback). 3 pointer files, 2 levels: config.json (host: name→version) · remoteEntry.js (remote: modules→chunks) · mf-manifest.json (remote: metadata).

Communication (loosest coupling wins)

ChannelWhen
URL / routerloosest — navigational/shareable state (L13). Nothing imports anything.
Props + callbackshost→remote, explicit typed contract, parent→child
Custom events / pub-subdecoupled sibling↔sibling: dispatchEvent(new CustomEvent(...)). Implicit/untyped — risk.
Shared storetightest — only truly global state (auth/cart). The "shared-state mess".
Type the contract: events/stores have no compile-time contract → producer changes payload, silently breaks consumers. Fix = shared versioned contracts package (names + types) both sides import. Never reach into another MFE's DOM/store — only the published contract.

Feature touching N domains

1. Challenge boundaries — touching every MFE = seams mis-drawn (follow business domains; Conway). 2. One owner (host/shell). 3. Host orchestrates via typed contracts, no reaching in. 4. Flag-gate rollout — remotes deploy independently, can't ship atomically. 5. Cross-domain transaction → BFF/backend, not browser events. Punchline: in a monorepo it's 1 atomic, typed PR — the strongest monorepo-over-MFE argument.

Rapid-fire

Monorepo (the often-better first answer)

MonorepoPolyrepo
Code sharingtrivial (local pkg)publish/version npm
Cross-cut changeatomic — 1 PR app+libmulti-repo dance
CIneeds affected+cachenaturally scoped
Why it scales = project graph + “affected” + caching: diff Git → rebuild/test only touched projects + dependents; hash inputs (src+config+env) → replay from local/remote cache (shared across team/CI). Cuts CI up to 90%.
Turborepo — simple, Rust-fast, caching + pipelines; no gen/graph/boundary. JS default.
Nx — full platform: graph viz, generators, module-boundary lint, distributed exec, remote cache. Big orgs.
Module-boundary enforcement (Nx) = killer Lead point: tag libs (scope:checkout,type:ui), fail CI if checkoutadmin or UI→data. Architecture as a lint rule — team boundaries at build time, no runtime federation.

Lead lines

MFE  "Micro-frontends decouple teams, not code. I adopt them when org coordination is the bottleneck — not for speed; they make the app slower."
Monorepo  "Monorepo and MFE are different axes — build-time layout vs runtime composition. I'd get 80% of the independent-teams win from a monorepo with affected builds + boundary lint, and add MFEs only for genuine independent deploys."
Deploy  "remoteEntry.js is a pointer at a stable URL — the one file I never cache, while the content-hashed chunks I cache forever. Cache the pointer and a deploy that purges old chunks white-screens every returning user. Versions come from a tiny runtime registry, so rollback is a one-line config flip, not a host redeploy."

Sources: martinfowler.com — Micro Frontends · Bitovi · LogRocket · ANGULARarchitects · module-federation.io · Nx mental model · Nx boundaries · Turborepo

Don't fail the interview

1. MFE = org solution, not perf. "Decouple teams, not code." Small team → monolith.
2. Monorepo (build-time) ≠ micro-frontend (runtime). Orthogonal. Don't conflate.
3. "Share code?" → that's a monorepo job. MFEs are for independent deploys; they make sharing harder.
4. Costs to name: dup bundles · version skew · lost types/fate-sharing · shared-state mess · ops.
5. Build-time integration (npm pkg) re-couples deploys → defeats the purpose. Runtime JS is the sweet spot.
6. singleton:true = one shared React; the version negotiation is where skew crashes a remote at runtime.
7. Platform: monorepo backbone (shared DS + boundary lint + affected CI) default; MFEs only at true deploy seams, SSR-composed to protect SEO/LCP.
8. Dep resolution: default = highest-compatible-else-duplicate; singleton = one copy, highest wins + warn; strictVersion = crash on mismatch. React MUST be singleton.
9. Communicate by loosest channel (URL → props → events → shared store) + type events with a shared contracts pkg. Never reach into another MFE's internals.
10. Feature spanning N MFEs → challenge the boundaries first; if real, host orchestrates via typed contracts + flag-gate rollout (independent deploys can't ship atomically). Monorepo = 1 atomic PR.
11. remoteEntry.js (the pointer) = no-cache; hashed chunks = immutable. Cache the pointer + purge old chunks = ChunkLoadError white-screen. Keep old chunks through the deploy.
12. Change/rollback a remote version without host redeploydynamic remotes + runtime manifest (repoint = rollback). Static build-time URLs need a host rebuild.
13. Dynamic ≠ "for freshness" (that's no-cache). Dynamic = runtime control of which URL/version/whether → pin/rollback/per-env/conditional. config.json registry = the one no-cache file; everything below it immutable.