Overview
Pick a monorepo when packages share types, when refactors routinely cross package boundaries, or when atomic releases matter (one PR ships frontend, backend, and shared types together). Pick polyrepo only when release cycles must diverge, security boundaries demand it, or the org has more than 50 engineers across teams that cannot coordinate on a single CI pipeline. The default for product teams under 50 engineers is monorepo with Turborepo or Nx. For larger orgs, the answer is usually still monorepo, just with heavier tooling (Bazel, Buck). See monorepo for the deeper rule set.
When monorepo wins
A monorepo is the right pick for any product team that shares code across packages.
- Atomic refactors: rename a TypeScript type and update every consumer in one PR. Polyrepo forces a multi-PR dance with version coordination.
- Shared types end to end: the backend’s
Usertype is the frontend’sUsertype, generated from the same Prisma schema or Zod schemas. See rest-vs-graphql for the tRPC variant. - One CI pipeline: one cache, one set of GitHub Actions, one set of secrets. Turborepo or Nx prunes builds to only changed packages.
- One PR review surface: cross-stack changes get reviewed together.
- Easier onboarding: clone one repo, run one command, get the whole stack.
- Best for product teams under 50 engineers and for any startup before Series B.
When polyrepo wins
Polyrepo is the right pick in three narrow cases. Default elsewhere is monorepo.
- Independent release cycles: a public SDK shipped to customers on its own version, a CLI distributed separately, an open-source library released on npm. These benefit from their own changelog, tags, and CI.
- Hard security boundaries: a payments service that contractually cannot share a repo with marketing-site code. Rare; usually overcautious.
- Org scale past 50 engineers with multiple business units: each unit may own its repo, but inside each unit the answer is still monorepo.
- Public open-source libraries where issue volume and contributor permissions need to be scoped per repo.
- Vendored or acquired codebases that come in as their own repos and have not yet been merged.
If the only reason for polyrepo is “we have always done it that way,” that is not a reason.
Trade-offs at a glance
| Dimension | Monorepo | Polyrepo |
|---|---|---|
| Cross-package refactors | One PR, atomic | Multi-PR, version coordination |
| Shared types | Trivial via workspace deps | Publish to npm or git submodule |
| CI complexity | One pipeline; needs caching tool | Many pipelines; each simple |
| Build tooling | Turborepo, Nx, Bazel, Buck | Standard per-repo tooling |
| Onboarding | One clone, one command | Per-repo setup; documented |
| Release independence | All packages share a tree | Each repo on its own cadence |
| Permissions | Repo-wide; CODEOWNERS scopes | Per-repo; cleaner ACLs |
| Disk and clone cost | Higher; needs sparse checkout past 1 GB | Lower per clone |
| Best fit | Product team, atomic refactors | Public libraries, independent SDKs |
| Common pitfall | No CI caching equals slow PRs | Drift between repos; version hell |
Migration cost
Polyrepo-to-monorepo is well-trodden and usually pays back inside one quarter. The reverse is rare and usually a sign of a different problem.
- Polyrepo to monorepo: use
git subtree add --prefix=apps/foo foo-repo main(orgit filter-repofor history rewrites). Migrate one repo at a time; keep the old repo read-only after migration as the history mirror. Plan one engineer-day per medium repo, plus a week to set up Turborepo or Nx, the sharedtsconfig, and CI caching. - Monorepo to polyrepo: usually triggered by a single package outgrowing the rest (one team wants its own release cadence). Extract that package; do not split the whole tree.
git filter-repo --subdirectory-filterpreserves the package’s history. - Avoid half-monorepos that share a folder structure but not workspaces. Either commit to workspaces (pnpm, yarn, npm) or stay polyrepo.
Recommendation
- New product, team under 50: monorepo with Turborepo or Nx, pnpm workspaces, shared
tsconfigandeslintconfigs. - Existing polyrepo with three or more repos that refactor together: migrate to monorepo. The friction signal is “PR A waits on PR B in another repo.” See monorepo.
- Public open-source library plus a private app that uses it: polyrepo. Publish the library to npm; consume it like any other dependency.
- Org with 200-plus engineers and multiple business units: monorepo per unit; Bazel or Buck if build graph is huge. See large-codebase.
- Acquired codebase: leave it polyrepo until you have a real reason to integrate it. Premature merging is its own footgun.
- CLI distributed as a binary, library shipped to customers: polyrepo; release cadence and changelog belong to the artifact.