Monorepos and lightweight tool choices
Monorepos and lightweight tool choices
A monorepo keeps the source for many projects in a single version-controlled repository. This article covers what a monorepo is, its history, how far a lightweight setup like pnpm workspaces can take us, and what full-scale tools (Nx, Turborepo, Lerna, Bazel, Rush) actually solve.
1. About monorepos
A code layout that places multiple packages and services in one repository. The opposite is a polyrepo (split repositories).
Notable examples:
- Google — Builds most products from a single repository (around 2 billion lines, internal tool Piper). Their internal build system Blaze became Bazel (2015) when released externally.
- Meta (Facebook) — Runs a huge Mercurial-based monorepo. Their build tools Buck / Buck2 are public.
- Microsoft — Built Git's virtual filesystem (VFS for Git → Scalar) and Rush to manage massive codebases like Windows on top of Git.
2. Practical value
- Changes spanning multiple packages can ship in a single PR.
- Internal libraries are referenced by relative path / workspace without being published to npm.
- Tool config (
tsconfig, lint rules, CI) is managed in one place.
In return, as the repo grows the following gets harder:
- Figuring out which package changed and rebuilding/testing only what's affected.
- Sharing a result cache so we don't rebuild from scratch every time.
- Drawing a dependency graph between packages so tasks run in the right order.
These three are the core of what full-scale monorepo tools solve.
3. Just pnpm workspaces
pnpm-workspace.yaml and the "workspace:*" protocol alone give us:
- Workspace dependency resolution (referring to internal packages).
pnpm -r <script>to run the same script in every package.pnpm --filter <name>to run only in a specific package.
What they don't give:
- Incremental builds based on a dependency graph (only what changed).
- Remote cache (sharing cache with other machines and CI).
- Task graph visualization or code generators.
Small monorepos (5–10 packages, CI time still tolerable) often run fine on pnpm workspaces alone.
4. Full-scale tools
Lerna (2015) — Originally a tool for publishing monorepo npm packages all at once. Maintenance moved to Nrwl (the Nx team) in 2022. It now lives on as a partial integration with Nx.
Turborepo (2021) — Started by Jared Palmer and acquired by Vercel the same year. Later rewritten in Rust. The core is task pipelines plus local and remote caching. Define task dependencies in turbo.json, and only changed (and affected) packages run, with results cached by hash key. Linking a Vercel account enables remote cache sharing.
Nx (Nrwl, 2017) — From the Angular CLI team. Provides task graph, remote cache, code generators, and an executor abstraction. The feature surface is wider than Turborepo's, with a steeper learning curve. Plugins exist for .NET and Java in addition to JS / TS.
Bazel (Google, 2015 OSS) — The public release of Blaze. Language-agnostic, with precise input tracking, deterministic (reproducible) builds, and strong remote caching / remote execution. The explicit BUILD-file config carries a high cost for small teams, but the payoff in giant monorepos is clear.
Rush (Microsoft, 2017) — Born from Microsoft's experience running large JS / TS monorepos. Works with PNPM, npm, or Yarn as a backend, and is strong on policies and a change-log workflow (rush change).
5. Common shapes
A small monorepo with pnpm workspaces:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
// apps/web/package.json
{
"name": "web",
"dependencies": {
"@org/ui": "workspace:*"
}
}
pnpm install # all packages
pnpm --filter web dev # only web
pnpm -r build # build every package
Adding Turborepo:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": { "dependsOn": ["build"] }
}
}
6. Common pitfalls
Tool too big for the problem — adopting Bazel for two or three packages tends to cost more than the BUILD files save.
Remote cache isn't always a win — when cache invalidation keys (input hashes) are inaccurate, hit rates drop or wrong cache hits hurt debugging.
Lerna's identity shifted after the acquisition — older guides (2018–2020) may describe behavior that no longer holds.
Turborepo's remote cache defaults to a Vercel account — for self-hosted needs, a separate server (the OSS turborepo-remote-cache) is required.
Tightly coupled packages — even when folders sit together, keeping the dependency direction one-way pays off in maintenance.
Closing thoughts
Small to mid-size monorepos run fine on pnpm workspaces alone. Full-scale tools like Turborepo and Nx fit when a dependency graph and cache sharing become essential. The real value of a monorepo is making changing things together easy, not the tool itself.
Next
- python-uv
- env-and-secrets
References include pnpm Workspaces, Turborepo docs, Nx docs, Bazel docs, Rush docs, Lerna docs, monorepo.tools, and Why Google Stores Billions of Lines of Code in a Single Repository (CACM, 2016).