Monorepos

A monorepo is a single version-controlled repository holding many distinct projects — the web app, the API, the mobile app, shared libraries, infrastructure — instead of one repo per project. Google, Meta, and Microsoft famously run colossal ones; more relevantly, the pattern has become standard for product teams whose frontend, backend, and shared packages evolve together.

The core promise is killing cross-repo coordination cost: shared code is imported directly instead of published/versioned/upgraded across repos, and a change to a shared type plus every consumer of it lands in one atomic commit — reviewed together, tested together, deployed together.

TL;DR

The Problem It Solves

Polyrepo sharing, honestly rendered:

Version drift, n× upgrade PRs, and "which version of shared is prod on?" archaeology. The monorepo version:

There is no version to drift — main is the version. That single property is why teams with heavily shared code (design systems, shared API clients, common domain types between a Next.js app and a Node API) gravitate here.

Quick Example

A typical TypeScript monorepo — pnpm workspaces + Turborepo:

dependsOn: ["^build"] means "my dependencies' builds run before mine" — the task graph mirrors the dependency graph, and caching keys on input hashes make unchanged work free.

Core Concepts

Not a Monolith

The perennial confusion: a monorepo is a source layout; a monolith is a deployment unit. A monorepo can contain thirty independently deployed microservices; a monolith can live in a polyrepo. The four combinations all exist — repo strategy and runtime architecture are separate decisions.

Task Graphs, Caching, and Affected Detection

The tooling problem is scale: 40 packages × every commit = unusable CI unless work is skipped safely. Three mechanisms do it:

Remote caching shares the cache across machines: CI builds packages/ui once; every later pipeline and every developer laptop downloads the result. On mature setups this is the difference between 40-minute and 4-minute CI.

The Tooling Landscape

Pragmatic guidance: JS/TS product teams → pnpm + Turborepo (or Nx if you want its structure and rules). Mixed-language at serious scale with a platform team → Bazel-class tools. Don't adopt Bazel for a three-app TypeScript repo.

CI and Deployment in a Monorepo

Monorepo vs Polyrepo

The deciding question is coupling: do these projects change together? Frontend + its API + shared types: emphatically yes → monorepo. Two unrelated products with separate teams, compliance boundaries, or open-source components: the isolation of polyrepo may serve better. Many orgs land on several monorepos, one per product area — the pattern is a dial, not a binary.

Common Mistakes

Monorepo Without the Tooling

Plain npm run build scripts looping over packages means every commit rebuilds everything; CI hits 40 minutes; developers revolt and blame the pattern instead of the missing cache. Task-graph tooling is not optional past ~5 packages.

The shared/ Dumping Ground

One packages/utils importing everything and imported by everything makes every change "affected everywhere" — destroying both caching and comprehension. Split shared code by domain (ui, api-client, dates), and use boundary rules (Nx enforce-module-boundaries, ESLint no-restricted-imports) to keep apps from importing each other.

Ignoring Ownership

One repo doesn't mean one team owns everything. CODEOWNERS per directory, required reviews on shared packages, and per-app deploy permissions keep the social structure that polyrepos gave you for free.

Cross-Project Coupling Creep

Because imports are easy, apps quietly reach into each other's internals. The monorepo makes coupling cheap to create and cheap to see — add the lint rules that make the wrong imports fail CI, or you'll rebuild the distributed monolith in source form.

Migrating Everything at Once

The graveyard is full of six-month big-bang consolidations. Start with the highest-coupling cluster (web + api + shared types), prove the workflow, then pull in projects as their coordination pain justifies it.

FAQ

Doesn't Git choke on huge monorepos?

At Google/Meta scale, yes — they run custom VCS layers. At normal-company scale (even thousands of packages), Git is fine, helped by shallow clones, sparse checkout, and partial clone in CI. You'll hit CI-time problems (solved by caching) long before Git problems.

How do open-source projects fit in?

Fine — many of the biggest OSS projects (Babel, Jest, React itself) are monorepos publishing many npm packages, with Changesets/Lerna-style release automation handling versions at the publish boundary.

Can different projects use different dependency versions?

Yes — workspaces resolve per-package package.json, so apps/legacy can hold React 17 while apps/web runs 19. Whether you want that divergence long-term is a policy question; the tooling permits it as a migration state.

What about very different stacks — Go API, TS frontend, Python ML?

Workable, with eyes open: either language-native workspaces side by side with a thin orchestrator (Turborepo runs arbitrary commands), or Bazel/Pants for a unified hermetic graph. The value still hinges on coupling — if the Python ML repo never changes with the frontend, co-locating buys little.

Is Turborepo or Nx "winning"?

Both are healthy defaults with converging features (Nx even supports Turborepo-style minimal adoption). Turborepo optimizes for staying out of your way; Nx for structure, codegen, and enforced architecture. Choose by how much framework you want around the task runner — migration between them is not traumatic.

Related Topics

References