Command Palette

Search for a command to run...

Hectal
PHASE 22Advanced ~27 min· topic 6 of 7

Topic 22.6

Large Apps: Monorepos, Micro-Frontends and Module Boundaries

In one line

When many teams share one frontend, a monorepo keeps several apps and packages in one repository with shared tooling, and micro-frontends split one product into separately deployed apps composed at runtime (for example with module federation); both enforce module boundaries, both have real costs, and most apps are better served by a well-structured single app first.

Think of it like this

A shopping centre. One option is a single big shop (one app): one owner, one till, easy to keep consistent. Another is one building with many shops that share the car park, the cleaning and the security (a monorepo): separate shops, shared services, one management. The third is independent shops that each open, close and renovate on their own schedule, joined by corridors with agreed rules (micro-frontends). Each layout suits a different number of owners, and each costs something.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Module boundary
A line in the code that others may cross only through a declared interface.
Monorepo
One Git repository holding several apps and packages that are developed together.
Workspace
A package-manager feature that links local packages in a monorepo so they can depend on each other.
Turborepo / Nx
Build orchestrators that run tasks across a monorepo in dependency order, only for what changed, with caching.
Micro-frontend
A separately built and deployed part of a frontend, owned by one team and composed into the page by a host.
Host (shell)
The app that loads micro-frontends and decides where each one appears.
Module federation
A bundler feature that lets one app load modules from another app's deployment at runtime and share dependencies.
Singleton dependency
A library (like React) that must exist only once on the page, so all apps share the same copy.

Step by step

01Grow in steps: boundaries first

Start with one app and feature folders enforced by lint (Topic 22.1). When a second app needs the same UI (an admin panel), move shared code into packages in a monorepo. Only when several teams are blocked on one release does splitting deployment pay off.

Grow in steps: boundaries firstdiagram
Rendering diagram…

02A monorepo layout

Apps are deployable; packages are shared libraries. The exports field of each package is its public API, which is a stronger boundary than a convention: deep imports into a package simply don't resolve.

repository rootwhole filebash
acme/
  apps/
    web/        package.json  ("@acme/ui": "workspace:*")
    admin/      package.json  ("@acme/ui": "workspace:*")
  packages/
    ui/         package.json  "exports": { ".": "./src/index.js" }
    config/     eslint, tsconfig and vite presets
  pnpm-workspace.yaml   packages: ["apps/*", "packages/*"]
  turbo.json
  package.json

03Let the orchestrator build only what changed

"dependsOn": ["^build"] means "build my dependencies first". With caching, a change to apps/admin doesn't rebuild or retest apps/web, and a teammate's CI run can reuse your cached results.

turbo.jsonwhole filejson
{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "test": { "dependsOn": ["^build"] },
    "lint": {}
  }
}

04Micro-frontends: a host composes remotes at runtime

Each remote is deployed at its own URL. The host loads its entry file when needed and shares one copy of React with it.

Micro-frontends: a host composes remotes at runtimediagram
Rendering diagram…

05Configure module federation (Vite plugin)

The remote exposes modules; the host lists remotes by URL; both mark React as a shared singleton. The webpack and Rspack plugins use the same option names.

vite.config.js (host and remote)whole filejavascript
// reviews (remote)
federation({
  name: "reviews",
  filename: "remoteEntry.js",
  exposes: { "./Reviews": "./src/Reviews.jsx" },
  shared: { react: { singleton: true }, "react-dom": { singleton: true } },
});

// shop (host)
federation({
  name: "shop",
  remotes: {
    reviews: { type: "module", name: "reviews", entry: "https://reviews.example.com/remoteEntry.js" },
  },
  shared: { react: { singleton: true }, "react-dom": { singleton: true } },
});

// in the host's code
const Reviews = lazy(() => import("reviews/Reviews"));

Try it yourself

  1. 1

    Add a second event

    In the first example, give the cart a Clear button that emits cart:clear, and make the catalog show Cart was cleared when it hears it. Which code had to know about the other team's internals? None: you extended the contract.

  2. 2

    Break the version promise

    In the remote example, change requires: "19" to requires: "20". Predict what the slot shows and whether the rest of the page still works.

  3. 3

    Simulate a remote outage

    Change <RemoteSlot url={remoteUrl} ...> to url="data:text/javascript,throw new Error('down')". Predict the screen. This is why every remote needs a fallback, ideally an error boundary too.

Code & diagrams

Two micro-frontends, two React roots, one event contract New tab

The two apps share no state, no context and no imports: only the `cart:add` event and its `{ name }` payload, which is the whole contract. Unmounting one app leaves the other running. In production each `mount` function would come from a different deployment.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

event cart:add { name: 'Tea' }
event cart:add { name: 'Coffee' }
Cart: 2 (Tea, Coffee)
catalog unmounted, cart still shows: Cart: 2 (Tea, Coffee)
Loading a remote at runtime and sharing one React (the module federation idea) New tab

The host downloads code it has never seen at build time, checks a version promise, and passes its own React in so only one copy exists. If the remote's URL is down, the host shows a fallback instead of crashing: every remote is a network dependency that can fail. Click the stars: the remote's own state works.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

loaded remote reviews needs React 19 - host has 19.2.8
slot shows: Reviews for product 7: ★★★★
Workspace dependencies in a monorepo (pnpm)bash
$ cat apps/web/package.json
{
  "name": "@acme/web",
  "dependencies": {
    "@acme/ui": "workspace:*",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  }
}

$ pnpm --filter @acme/web dev          # run one app
$ pnpm turbo run build --filter=...[origin/main]
# builds only the packages changed since main, plus everything that depends on them

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Two copies of React on one page

Let a remote bundle its own react instead of sharing the host's (forget singleton: true), and render a remote component that calls useState inside the host's tree.

terminal
$ Console
── what you'll see ──
Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.

Break #2

A shared global store between micro-frontends

Put a Redux store on window.store and let every micro-frontend read and write any slice of it.

terminal
$ Release day
── what you'll see ──
Checkout team renames state.cart.items to state.cart.lines; catalog's deployed bundle still reads items, and 'Add to cart' silently stops updating the badge in production.

Myth vs fact

Myth

Micro-frontends make apps faster.

Fact

They usually make the first load slower (more requests, duplicated code). They make teams faster by removing release coordination.

Myth

A monorepo means one giant app.

Fact

A monorepo can hold many independently deployed apps. It's about where code is stored and how it's built, not how it's deployed.

Myth

Module federation is required for micro-frontends.

Fact

It's one runtime approach. Others include iframes, server or edge composition, web components, and a simple mount()/unmount() contract (single-spa).

Myth

Large companies always need micro-frontends.

Fact

Many large products (including big React codebases) run as a single app or a monorepo with strong boundaries. Choose by team structure and release pain, not company size.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Unmounting a nested React root inside the host's effect cleanup can warn that you "attempted to synchronously unmount a root while React was already rendering"; defer the remote's unmount() with queueMicrotask or setTimeout when the host tears down during a render.

  • ▸

    Module federation 2.0 (@module-federation/enhanced and the Vite/Rspack plugins) adds a runtime API (loadRemote), type hints generated from remotes, and a manifest (mf-manifest.json) so hosts can discover remote versions without hard-coding URLs.

  • ▸

    Server-rendered frameworks complicate runtime composition: each remote must render on the server too, or be client-only islands. Next.js's App Router doesn't support module federation officially; its answer for multiple teams is Multi-Zones (separate Next.js apps behind one domain, joined by path rewrites).

  • ▸

    Conway's law (systems mirror the communication structure of the organisations that build them) is the real driver: draw module boundaries where team boundaries are, so most changes stay inside one team's code.

Remember this

  1. 1

    A module boundary is a line in the code that other code may only cross through a declared interface: a feature's index.js (Topic 22.1), a package's exports field, or a micro-frontend's mount() function. Boundaries let teams change their insides without asking anyone. The bigger the organisation, the stronger the boundary needs to be: a lint rule, then a package, then a separate deployment.

  2. 2

    A monorepo is one Git repository holding several projects: for example apps/web, apps/admin, packages/ui, packages/config. A workspace tool (npm, pnpm or Yarn workspaces) links local packages together, so apps/web can depend on "@acme/ui": "workspace:*" and see changes instantly. A build orchestrator (Turborepo or Nx) knows the dependency graph, runs tasks in the right order, runs only what changed, and caches results locally or remotely. Benefits: one place for shared UI and configs, atomic changes across apps (change the Button and every app in one pull request), one set of tool versions. Costs: tooling to learn, CI that must be smart about what to build, and repository-wide upgrades.

  3. 3

    A micro-frontend architecture splits one product into several frontend apps owned by different teams and deployed independently: the catalog team ships at 10:00, the checkout team at 15:00, without coordinating a release. A host (or shell) app composes them into one page. Composition can happen at build time (each micro-frontend is an npm package, which loses independent deployment), on the server (edge or server-side includes), or at runtime in the browser, which is the most common.

  4. 4

    Module federation is the best-known runtime approach. It started as a webpack 5 plugin and now also exists for Rspack and Vite (@module-federation/vite). A remote app builds a small remoteEntry.js that exposes modules (./Reviews). The host loads that file from the remote's URL at runtime and imports the exposed module as if it were local: const Reviews = lazy(() => import("reviews/Reviews")). Shared dependencies are negotiated so the page loads one copy of React: you mark react and react-dom as singleton. Two copies of React on one page break hooks and context.

  5. 5

    A simpler runtime contract, used by single-spa and many in-house setups, is that every micro-frontend exports mount(element, props) that returns unmount(), each mounting its own React root. Communication goes through narrow channels: URL changes, browser CustomEvents with documented names, or props from the shell. Shared global stores across micro-frontends recreate the coupling you were trying to remove.

  6. 6

    Be honest about the costs. Micro-frontends add network requests and duplicate code (bigger downloads, slower first load), make shared design and accessibility harder to keep consistent, need version agreements for shared libraries, complicate local development and end-to-end testing, and spread one user journey across several teams' failure modes. They solve an organisational problem (many teams blocked on one release train), not a technical one. If you have one or two teams, a single app with feature folders and lint boundaries, or a monorepo, gives most of the benefits at a fraction of the cost.

Explain it without notes

01

What is a monorepo and what problems does it solve for a React codebase?

02

What are micro-frontends, and when would you recommend them?

03

Explain module federation and why React must be shared as a singleton.

04

What are the costs of micro-frontends?

Practice

01

Write a mount(el, props) contract for a Newsletter micro-frontend that also supports updating props later (update(props)) and unmounting.

02

Write a pnpm-workspace.yaml and the package.json dependency that lets apps/web use a local @acme/ui package.

Trade-offs

  • ↔

    Single app: simplest and fastest for users; boundaries depend on discipline and lint rules. Monorepo: shared code and atomic changes; needs build tooling. Micro-frontends: independent deploys; pays in performance, consistency and operational complexity.

  • ↔

    Runtime composition gives the most independence and the most failure modes; build-time composition (packages) is safer but brings back coordinated releases.

Done when you can

  • I can explain module boundaries at the folder, package and deployment level.

  • I can describe a monorepo layout with workspaces and Turborepo or Nx.

  • I can explain what micro-frontends are and when they are worth their costs.

  • I can explain module federation, remotes, exposes and shared singletons.

  • I can design a mount/unmount contract and an event-based channel between micro-frontends.