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

DimensionMonorepoPolyrepo
Cross-package changeOne atomic PROne PR per repo plus version coordination
Shared typesWorkspace dependencyPublish a package or generate from a schema
CIOne pipeline; needs caching and affected-only runsMany simple pipelines
Access controlRepo-wide; CODEOWNERS scopes reviewPer-repo permissions
ReleasesShared tree; independent versioning needs toolingEach repo on its own cadence
Clone costLarger; use partial or sparse cloneSmall
Typical failureSlow CI with no cachingVersion 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> main keeps history; git filter-repo rewrites 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.