Overview
A monorepo holds multiple shippable artifacts in one git repo. Done well, it lets a single PR refactor a shared library and every consumer at the same time. Done poorly, it becomes a slow CI graveyard where every change rebuilds the world. This page covers when the model pays off, the workspace and build tooling worth using, and the costs that show up six months in. See project-structure for the single-repo default.
Use a monorepo only with a concrete reason
Default to many repos. Reach for a monorepo when at least one is true:
- Two or more projects share a library that changes in lockstep. Coordinated cross-repo releases cost more than a shared CI.
- Atomic refactors across services are routine (renaming a shared type, changing a wire format).
- The team is small enough that one PR can land everywhere without three reviewers blocking.
Cargo-culting a monorepo because “Google does it” wastes a quarter. Google has dedicated build infrastructure teams; most teams have GitHub Actions and a six-minute CI budget.
Workspaces are the entry point
Native workspaces are the cheapest monorepo on-ramp. Reach for them before installing a third-party build graph.
- pnpm workspaces. Best Node story.
pnpm-workspace.yamllists packages;pnpm installsymlinks them. Fast, deterministic. - Yarn workspaces (Berry). Works but the v1-vs-Berry split is a tax. Prefer pnpm unless already on Yarn.
- npm workspaces. Functional, slower than pnpm.
- Cargo workspaces. A root
Cargo.tomlwith[workspace]andmembers = [...]. Trivial. Use it. - Go workspaces (
go.work). Use for local cross-module dev; do not check ingo.work.sum.
A workspaces-only repo carries five to twenty packages comfortably. Past that, the build graph needs help.
Add a build graph when CI hurts
Above ~20 packages or when CI exceeds 10 minutes on a small change, add a task runner that understands the dependency graph and caches.
- Turborepo. Best balance for Node monorepos. Config in
turbo.json. Remote caching via Vercel or self-hosted. - Nx. More features (generators, code mods, project graph UI). Worth it past ~50 packages or for non-JS code.
- Bazel / Buck2 / Pants. Polyglot, hermetic, slow to set up. Only worth it with a dedicated build engineer.
Migration order: workspaces, then Turborepo or Nx, then Bazel. Skipping to Bazel for five packages is a footgun.
CI sharding is mandatory at scale
A 50-package monorepo cannot rebuild everything on every push. Shard by what changed.
- Use
turbo run --filter=...[origin/main]or Nx’s affected commands to limit builds to the touched packages. - Cache build outputs by input hash so a no-op rebuild is a cache hit.
- Split test runs across parallel CI jobs via GitHub Actions matrices.
- Tag long-running integration tests so PR CI runs the fast path and nightly CI runs the full suite.
Without sharding, the monorepo becomes the bottleneck it was supposed to fix.
CODEOWNERS at the package level
A monorepo erases natural team boundaries. CODEOWNERS puts them back. List ownership per package directory:
/packages/billing/ @org/payments-team
/packages/auth/ @org/identity-team
/apps/admin-dashboard/ @org/admin-team
Without CODEOWNERS, the “anyone can review anything” failure mode dominates and code quality drifts package by package. See large-codebase for the broader pattern.
Hidden costs to budget for
The pitch is “one PR refactors everything.” The bill arrives later.
- CI complexity. Sharding, caching, and matrix configs are a maintained codebase of their own.
- Single-team gravity. The repo becomes “owned” by whoever fixes its build first; other teams stop contributing.
- Tooling locks. A bug in the chosen build tool blocks the entire repo. Vet maintenance health first.
- Slow git. A 5 GB repo with 100k files makes
git statusslow withoutcore.fsmonitor. Use partial clone or sparse checkout. - IDE indexing. TypeScript project references and editor performance degrade past ~50 packages.
Poly vs mono: a deciding question
Ask: “Does a typical change touch one package or many?”
- One package. Polyrepo wins. Independent CI, independent ownership, independent release cadence.
- Many packages. Monorepo earns its keep. Atomic refactors are the killer feature.
If the answer is “depends,” start polyrepo and merge later when the shared-library pain becomes the daily pain. See git for the commit hygiene that makes either model tolerable, and monorepo-vs-polyrepo for the head-to-head selection rules.