Back to Blog
Feb 25, 20264 min readOnuzulike Anthony

Monorepo vs Polyrepo: The Real Trade-offs

Turborepo makes monorepos tractable but doesn't make them free. When shared packages make sense, when they don't, and what you're signing up for.

ArchitectureArchitectureDevOpsTooling

The monorepo vs polyrepo debate is usually framed as a philosophy argument. It's more useful to frame it as an engineering trade-off with specific costs on each side. The right answer depends on the number of teams, the dependency graph between packages, and how often you need to make atomic cross-package changes.

What You Get From a Monorepo

Atomic commits across packages. If you change the type signature of a shared utility and need to update every consumer, one commit covers all of them. In a polyrepo, this is three PRs and three deployments with a window where v1 of the util is deployed but v2 of the consumer isn't.

Shared tooling config. One biome.json, one tsconfig.base.json, one CI pipeline. Consistency is automatic rather than enforced through copying.

Refactoring visibility. When you rename a function, your IDE's "Find references" crosses package boundaries. You can be confident you've caught all usages before merging.

Instant feedback on breaking changes. If package A exports a type that package B depends on, changing that type breaks B's build immediately — in the same PR, not after publishing a new version.

What You Pay For

CI complexity. A monorepo CI pipeline needs to understand which packages changed and which downstream packages need to be rebuilt and retested. Without tooling like Turborepo or Nx, this becomes a brute-force "run everything" pipeline that takes 45 minutes.

Merge conflicts. On a large team, everyone's working in the same repo. Lock file conflicts (package-lock.json or bun.lock) become a tax on every merge.

Repository size. If you have teams that only care about one part of the codebase, they still clone the whole thing, run into build steps from unrelated packages, and need to understand the larger structure.

Dependency discipline. Shared packages in a monorepo are easy to reach for as shortcuts. Teams start importing directly from sibling packages instead of defining proper interfaces, creating invisible coupling.

Turborepo's Contribution

Turborepo solves the CI problem. Its dependency graph-aware task runner understands that building apps/web requires first building packages/ui and packages/api. It caches outputs — if packages/ui hasn't changed, it doesn't rebuild it.

json
// turbo.json
{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    }
  }
}

^build means "run build in all dependencies first." Turborepo handles the topological sort. Remote caching (via Turborepo's cloud or self-hosted) shares build artifacts across CI runs — the first run builds; subsequent runs that haven't touched a package restore from cache.

When to Split Into a Polyrepo

Split when:

  • Different release cadences and ownership. If the backend team deploys 10 times a day and the frontend team deploys weekly, shared CI and review processes create friction that outweighs the benefits.
  • Genuinely independent products. If two products share no code and never will, there's no benefit to shared build infrastructure.
  • Security boundaries. If one package handles PCI-scoped data and you want to limit developer access to it, separate repos with separate access controls are cleaner than monorepo subtree permissions.

The Middle Ground: Internal Packages Without a Framework

For projects with 2–4 packages sharing a significant amount of code, a simple workspace setup (npm/pnpm/bun workspaces) without Turborepo is often the right starting point. Add task runners when you hit actual CI performance problems, not preemptively.

json
// package.json (root)
{
  "workspaces": ["apps/*", "packages/*"]
}

The monorepo vs polyrepo decision is ultimately about team topology more than technology. If two teams need to coordinate changes frequently, put them in the same repo. If they can deploy and iterate independently, separate repos let them do that without stepping on each other.