Library
00/07 · ~34 min
GUIDEDECK · structuring frontends that grow with the team

Modular & Micro-Frontend
Architecture
for frontends that scale.

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.

~34 MINBEGINNER → INTERMEDIATEFRONTEND / BUILD
SCROLL
01 · Why modularize 4 min

A frontend is just code —
the same boundaries rules apply.

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.

Module — a self-contained unit of code with a clear public surface and hidden internals. On the frontend a module might be a folder, an npm package, or a whole independently-built app. The unit changes; the goal never does: group what changes together, and keep a thin, explicit boundary between groups.

The two enemies, in the browser

  • Coupling — a deep import reaches across the app (../../checkout/internals/tax). Touch one feature and an unrelated screen breaks in CI.
  • Low cohesion — one 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.

TANGLED cartpay authfeed BOUNDED shared contract cartpay authfeed

Left: every feature imports every other. Right: features meet only through a shared, explicit contract.

Symptom

The 40-minute build

A one-line copy change rebuilds and redeploys the entire app. Slow feedback is a boundary problem, not a CI problem.

Symptom

Merge-conflict tax

Five squads editing one routes.tsx means every PR fights every other. Ownership is unclear because the seams are.

Goal

Change locality

The whole agenda: make a change touch one well-named place, and let the blast radius stop at a boundary you can see.

02 · Monorepo vs polyrepo 5 min

Where the code lives
is the first boundary decision.

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.

Monorepoone repository holding many apps and shared packages, built and versioned together. Polyrepomany repositories, one per app or library, each versioned and released on its own. Neither is "micro-frontends"; both are just where files sit.
MONOREPO repo/ apps/shop apps/admin packages/ui packages/utils one install · one PR · atomic POLYREPO shop repo admin repo ui-lib repo npm registry publish · pin a version · consume

Monorepo: shared code is a folder you import directly. Polyrepo: shared code is a published package you pin a version of.

Workspaces & shared packages

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.

# pnpm-workspace.yaml — declare the members packages: - "apps/*" - "packages/*"
{ "name": "shop", "dependencies": { "@acme/ui": "workspace:*" // sibling, not from npm } }
Monorepo — shines when
  • One change spans app + shared lib — land it in one atomic PR, no version dance.
  • You want consistent tooling, lint, and types across everything.
  • Tradeoff: needs a smart build tool (next section) or CI rebuilds the world.
Polyrepo — shines when
  • Teams want hard release independence and separate access control per repo.
  • A library has external consumers and a real semantic-version contract.
  • Tradeoff: cross-cutting changes become a multi-PR, version-bump chore.

Monorepo ≠ monolith, polyrepo ≠ micro-frontends.Code layout and runtime architecture are independent choices — don't conflate them.

03 · The modular monolith (frontend) 5 min

One app, many clean modules
and usually that's enough.

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.

Modular monolith — a single deployable frontend internally divided into feature modules, each owning its UI, state, and data access, talking to others only through a published surface. One build, one deploy — many well-fenced rooms inside it.
src/ features/ checkout/ index.ts // the ONLY public door ui/ model/ api/ // internals — private catalog/ index.ts ui/ model/ api/ shared/ // design system, types // rule: import from "features/x", never "features/x/ui/..."
one build · one deploy checkout index.ts catalog index.ts shared · design system

Each feature exposes one index.ts door; everything else is private. Shared sits underneath, depended on by all.

Make the boundary real

  • Barrel exports — a feature's index.ts is its public API; deep paths stay internal.
  • Lint the seams — an import-boundary rule (ESLint no-restricted-imports or an Nx/Sheriff boundary config) fails the build on a reach-around.
  • Dependency direction — features depend on shared, never on each other's internals. Same "point dependencies inward" rule as Architecture Patterns.
  • Lazy-load per route— code-split features so a big app still ships small bundles. You get most of the "independent" feel with none of the ops cost.

When it is enough (most of the time)

  • One framework version, one router, one design system — shared for free, no duplication shipped to users.
  • Refactors are a normal PR, not a cross-repo migration.
  • A single bundle is easy to optimize: one vendor chunk, one set of Core Web Vitals to watch.

Like a well-organized house with locked rooms — not a street of separate buildings you have to walk between.

04 · Micro-frontends 6 min

Splitting the UI into
independently deployable apps.

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.

Micro-frontend — an independently deployable piece of a UI, owned end-to-end by one team. The defining test is the deploy boundary: can a team ship their slice to production without coordinating a release with anyone else? If not, it is just a module.

Three ways to integrate the pieces

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.

Build-time

Compose as packages

@team/cart @team/feed container one build re-deploy to update ✕

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.

Server-side

Stitch on the server

cart svc feed svc edge stitch HTML fast first paint ✓

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.

Run-time

Load in the browser

shell cart.js feed.js pay.js fetch at runtime ✓

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.

05 · Module Federation 5 min

Sharing code between apps
at runtime.

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.

Module Federation — a bundler capability (originating in Webpack 5, now also in Rspack and via a Vite plugin) where a remote publishes modules and a host imports them at runtime by URL. Crucially, both declare shared dependencies so a singleton like React is loaded once.
new ModuleFederationPlugin({ name: "feed", filename: "remoteEntry.js", exposes: { "./Feed": "./src/Feed" }, // public shared: { react: { singleton: true } }, })
const Feed = React.lazy(() => import("feed/Feed")) // resolved over the network // renders the team's latest deploy — no host rebuild
host shell app remote · feed remoteEntry.js import() shared: react singleton · loaded once

The host imports feed/Feed from the remote's remoteEntry.js; both share one React instance.

The win

Deploy independence

Ship the remote and the host picks it up on next load — no coordinated release. This is the capability you were buying.

The catch

Shared-version risk

A singleton mismatch (two React majors) breaks hooks at runtime. You must govern shared versions across teams — contracts move from compile-time to runtime.

The catch

Runtime failure modes

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.

06 · The tooling landscape 5 min

Two toolboxes:
repo orchestration and UI integration.

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.

Make one big repo fast — pick by ambition

pnpm workspaces

The baseline

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.

Turborepo

Fast task runner

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.

Nx

Batteries-included

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.

How to choose
Start with pnpm workspaces. Add Turborepo when CI gets slow. Reach for Nx when you need enforced boundaries and an affected graph across many apps.

Stitch separate UIs — only if you truly need to

Module Federation

Runtime sharing

Bundler-level remotes with shared deps (Webpack/Rspack; Vite plugin).

Pro — real deploy independence, deduped libraries.

Con — runtime version coupling; build config to master.

Vite federation

MF for Vite

A community plugin bringing federation to Vite/Rollup builds.

Pro — keeps a fast Vite dev loop.

Con — less mature; dev-vs-prod parity quirks.

single-spa

App orchestrator

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.

iframes

Hard isolation

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.

How to choose
Same framework, want dedup → Module Federation (Vite plugin if on Vite). Mixed frameworks or a migration → single-spa. Third-party or hostile content needing total isolation → iframes.

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.

07 · When NOT to · choosing · recap 4 min

Micro-frontends are powerful —
and over-adopted.

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.

×N

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.

1

A modular monolith pays none of this and gets you 90% of the modularity. Start here.

Choose deliberately

  • Default: modular monolith in a monorepo. Boundaries enforced by lint, features lazy-loaded.
  • Independent teams, one big product, deploy contention is real pain? Then run-time micro-frontends (Module Federation).
  • Migrating legacy / mixed frameworks? single-spa or iframes as a transition, not a destination.
  • The honest test: name the team and the deploy that the boundary unblocks. No name → no boundary.

"Do two teams need to deploy the same UI on different schedules, today?"

  • No → modular monolith. You are done.
  • Yes → micro-frontends, run-time integration, and budget for the governance.

Everything in this deck reduces to that question. See also the microservices trade-offs in Architecture Patterns & Styles.

1Boundaries first. Coupling and cohesion decide everything — the build is just where they show up.
2Repo layout ≠ architecture. Monorepo vs polyrepo is independent of monolith vs micro-frontends.
3Prefer the modular monolith. One build, fenced features, enforced imports — it wins until you truly need independent deploys.
4Independence has a price. Run-time integration buys deploy freedom and bills you in duplicated payload, runtime failures, and version governance.
5Adopt for a real org problem. Name the team and the deploy the boundary unblocks — or keep it simple.
Knowledge check

Did it stick?

Five quick questions on boundaries, repos, the modular monolith, micro-frontends, and Module Federation — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library