A 34-minute working session on drawing boundaries in the browser — from module coupling, through monorepos and the modular monolith, to micro-frontends and Module Federation. With one honest message: reach for the simpler option until you truly can't.
Everything you know about coupling and cohesion from OOP & Architecture travels straight into the browser. A modular frontend is one where you can find, change, and ship one feature without dragging the whole app along. This deck owns the concrete build angle — how those boundaries map onto files, packages, and deploys.
../../checkout/internals/tax). Touch one feature and an unrelated screen breaks in CI.utils/ folder owns date math, API calls, and a date picker. Nobody can name it, so everything imports it.Modularization is the cure both at the class level (OOP & Architecture) and the system level (Architecture Patterns & Styles, the backend equivalent). Here we apply it to the build.
Left: every feature imports every other. Right: features meet only through a shared, explicit contract.
A one-line copy change rebuilds and redeploys the entire app. Slow feedback is a boundary problem, not a CI problem.
Five squads editing one routes.tsx means every PR fights every other. Ownership is unclear because the seams are.
The whole agenda: make a change touch one well-named place, and let the blast radius stop at a boundary you can see.
Before any fancy architecture, you choose how repositories are laid out. This is independent of micro-frontends — a modular monolith and a micro-frontend system can each live in either layout. Get the vocabulary straight first.
Monorepo: shared code is a folder you import directly. Polyrepo: shared code is a published package you pin a version of.
A workspace is the monorepo feature (in npm, pnpm, or Yarn) that lets one app depend on a sibling package by name — @acme/ui — with no publish step. The package manager symlinks it; edits are instant across the repo.
Monorepo ≠ monolith, polyrepo ≠ micro-frontends.Code layout and runtime architecture are independent choices — don't conflate them.
Before splitting your app into separately-deployed pieces, get the cheap 90%: a single build, organized into feature modules with boundaries you actually enforce. This is the default we want you to reach for.
Each feature exposes one index.ts door; everything else is private. Shared sits underneath, depended on by all.
index.ts is its public API; deep paths stay internal.no-restricted-imports or an Nx/Sheriff boundary config) fails the build on a reach-around.shared, never on each other's internals. Same "point dependencies inward" rule as Architecture Patterns.Like a well-organized house with locked rooms — not a street of separate buildings you have to walk between.
A micro-frontend architecture extends the microservices idea to the browser: each slice of the UI is owned, built, and shipped by a separate team on its own cadence. Powerful — and, like microservices, frequently adopted before it is needed.
The whole problem is composition: how do separately-built slices become one page for the user? There are three integration moments — and they trade independence against simplicity very differently.
Each team publishes an npm package; the host installs and bundles them. Simple and type-safe — but a team's change only ships when the host re-deploys. Not truly independent.
An edge/server layer composes HTML fragments from each team's service into one document (SSI, ESI, or modern frameworks). Great first paint and SEO; needs server infrastructure to assemble.
A shell loads each team's bundle live, by URL. A team deploys their bundle and it appears with no host rebuild — true deploy independence. Module Federation lives here (next).
Only server-side and run-time give real release independence. If you adopt micro-frontends and stay at build-time, you have paid the complexity and kept the coupling.
Module Federation is the bundler feature that made run-time micro-frontends practical. One app (the host) loads code exposed by another (the remote) over the network, and they share common libraries so React isn't shipped five times.
The host imports feed/Feed from the remote's remoteEntry.js; both share one React instance.
Ship the remote and the host picks it up on next load — no coordinated release. This is the capability you were buying.
A singleton mismatch (two React majors) breaks hooks at runtime. You must govern shared versions across teams — contracts move from compile-time to runtime.
A remote can be down, slow, or 404. You now need fallbacks, error boundaries, and version checks the bundler used to give you for free.
Don't confuse them. Monorepo tools make a big repo fast to build and test; integration tools stitch separately-built UIs together. You often want a monorepo tool and no integration tool at all.
Just the package manager's workspace feature — links siblings, dedupes installs.
Pro — zero extra tooling; built in.
Con — no task caching or affected-graph; scripts rebuild everything.
A caching task graph over your existing scripts — only rebuild what changed.
Pro — tiny config, remote cache, great with pnpm.
Con — orchestration only; no generators or built-in boundary rules.
Caching plus a project graph, code generators, and enforceable module boundaries.
Pro— strongest boundary & affected-graph story at scale.
Con — more concepts to learn; can feel heavy for a small repo.
Bundler-level remotes with shared deps (Webpack/Rspack; Vite plugin).
Pro — real deploy independence, deduped libraries.
Con — runtime version coupling; build config to master.
A community plugin bringing federation to Vite/Rollup builds.
Pro — keeps a fast Vite dev loop.
Con — less mature; dev-vs-prod parity quirks.
A router that mounts/unmounts whole framework apps on one page.
Pro — mix React + Vue + Angular; clear lifecycle.
Con— heavier wiring; doesn't dedupe deps by itself.
The browser's own boundary — separate document per slice.
Pro — bulletproof CSS/JS isolation; any stack.
Con — clumsy routing, sizing, and shared state; duplicated payload.
Notice: nearly everyone needs a monorepo tool; far fewer need an integration tool. Adopt the second only when a deploy boundary is a real, present requirement — not a someday.
Like microservices, they solve an organizational problem (independent teams shipping independently), at a real technicalcost. If you don't have the org problem, you are buying the cost for nothing.
Duplicated framework payload shipped to users unless sharing is perfect — a real Core Web Vitals tax.
Runtime failure surface: every remote is a network dependency that can break the page.
Version governance across teams — one design system and framework major, coordinated forever.
A modular monolith pays none of this and gets you 90% of the modularity. Start here.
"Do two teams need to deploy the same UI on different schedules, today?"
Everything in this deck reduces to that question. See also the microservices trade-offs in Architecture Patterns & Styles.
Five quick questions on boundaries, repos, the modular monolith, micro-frontends, and Module Federation — instant feedback, no sign-in.
Navigate with ← → or scroll · back to library