Overview
Use one monorepo for code that changes together; split out only artifacts with their own release cadence or access rules. The test is the friction signal: “PR A waits on PR B in another repo” means the code belongs in one repo. A monorepo without task caching and affected-only CI gets slower with every package, so budget for Turborepo or Nx (or Bazel at very large scale) on day one.
Comparison
| Dimension | Monorepo | Polyrepo |
|---|---|---|
| Cross-package change | One atomic PR | One PR per repo plus version coordination |
| Shared types | Workspace dependency | Publish a package or generate from a schema |
| CI | One pipeline; needs caching and affected-only runs | Many simple pipelines |
| Access control | Repo-wide; CODEOWNERS scopes review | Per-repo permissions |
| Releases | Shared tree; independent versioning needs tooling | Each repo on its own cadence |
| Clone cost | Larger; use partial or sparse clone | Small |
| Typical failure | Slow CI with no caching | Version drift between repos |
Pick a monorepo when
Pick a monorepo for a product whose apps and packages evolve together.
- A renamed type or API field should update every consumer in one reviewed change.
- Frontend, backend, and shared schemas (Zod, Prisma, OpenAPI) must stay in sync.
- One set of lint, TypeScript, and CI configs should apply everywhere. Use pnpm workspaces (yarn-vs-pnpm).
- Mark packages that publish externally with their own versioning (Changesets or similar) rather than splitting the repo.
Pick polyrepo when
Pick polyrepo when separation is a requirement, not a habit.
- A public SDK, CLI, or open-source library with its own issues, contributors, changelog, and tags.
- Contractual or regulatory access boundaries that repo permissions must enforce.
- Acquired or vendored codebases that nothing else in the company imports yet.
- Business units that share no code and no release process; run a monorepo inside each unit.
Migrate
Move one repo at a time and keep the old repo read-only as a history mirror.
- Into a monorepo:
git subtree add --prefix=apps/foo <remote> mainkeeps history;git filter-reporewrites paths first when needed. Then add shared TypeScript config, workspaces, and CI caching. - Out of a monorepo: extract only the package that outgrew the tree, with
git filter-repo --subdirectory-filter <path>. - Do not keep a half-monorepo that shares a folder layout but no workspace tooling.
See monorepo for layout rules and large-codebase for very large trees.
FAQ
How do I know if code belongs in a monorepo?
If PR A waits on PR B in another repo, the code belongs in one repo. Split out only artifacts with their own release cadence or access rules.
Why is my monorepo CI so slow?
A monorepo without task caching and affected-only CI gets slower with every package. Budget for Turborepo or Nx on day one, or Bazel at very large scale.
Can packages in a monorepo be versioned independently?
Yes; mark packages that publish externally with their own versioning through Changesets or similar tooling instead of splitting the repo.
How do I merge repos into a monorepo without losing history?
Use git subtree add --prefix=apps/foo <remote> main, which keeps history, or rewrite paths first with git filter-repo. Move one repo at a time and keep the old repo read-only as a history mirror.