Monorepos are one of those tools that look magical in talks and feel painful in practice. Shared types! Atomic refactors! One PR for the whole stack! Also: 30-minute installs, fragile caching, mysterious CI failures, and onboarding documents that read like ancient lore.

The honest version is that monorepos help under specific conditions and hurt the rest of the time. Knowing which side you're on matters.

Monorepos Help When Sharing Is Real

The strongest case for a monorepo: you have actual code that is actually shared between actual deployable units, and that code changes together.

Text
my-app/
  packages/
    shared/        # types, errors, validators used by both
    api/           # backend, deploys to its own server
    web/           # frontend, deploys to its own CDN
    mobile/        # React Native app, deploys to stores

If shared/Order.ts changes, you want all three apps to use the new shape on the same day. With separate repos, you'd publish a new package version, update three repos, deploy three times, and live with version skew during the migration. With a monorepo, the change is one PR.

This is the right tool when:

  1. You have shared DTOs that mobile, web, and backend all consume
  2. You ship multiple apps from one team and they evolve together
  3. You want atomic refactors across components that depend on each other
  4. You centralize tooling (one TypeScript config, one ESLint config, one CI pipeline)

They Hurt When Everything Becomes Coupled

The dark side: a monorepo makes coupling cheap. Without discipline, every package starts importing from every other, the dependency graph turns into a hairball, and "atomic refactor" becomes "12-hour build."

Common pain points:

  1. Build slowness. A change in shared/ rebuilds half the world.
  2. Lockfile churn. Every pnpm install shuffles a 50,000-line lockfile.
  3. CI flakiness. Caching matters more than tests; one wrong cache key and you're rebuilding everything.
  4. Tool sprawl. Nx, Turborepo, Lerna, Bazel — each has its own config, its own quirks, its own breaking changes.
  5. Onboarding cliff. A new engineer needs to know the monorepo tool before they can read your code.

If your shared code isn't actually shared by multiple consumers, all of this cost buys you nothing.

Build Speed Is A Product Feature

A monorepo without good caching is a productivity disaster. Modern tools (Turborepo, Nx, Bazel) cache build outputs by content hash — if nothing changed in shared/, you don't rebuild it.

JSON turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    }
  }
}

Two metrics worth tracking:

  1. Cold cache build time — full clean install + full build. Acceptable if it's "got coffee" not "got lunch."
  2. Warm incremental build time — typical PR with one package changed. Should be seconds, not minutes.

If warm builds take more than a minute, the monorepo isn't paying back. Either fix the caching or split the repo.

Monorepo dependency graph. A central @acme/shared package holds DTOs and Zod schemas. Three consumer packages — apps/web, apps/mobile, apps/api — depend on it via the workspace protocol, which lets a single contract change update tsc, eslint, and tests across all three apps in the same PR.
Shared packages. One contract. Changes never drift between web, mobile, and the server.

Versioning Still Exists

A common monorepo myth: "no more versioning." That's only true within the repo. Once you publish anything externally — npm packages, deployed APIs, mobile app stores — versioning is back.

Tools like Changesets handle this for npm publishing: each PR includes a changeset describing what changed and at what semver level. The release workflow bumps versions and publishes affected packages. It's still version coordination, just centralized.

For internal-only monorepos (one company, one product), you can mostly skip semver and ship from main. Once external consumers exist, you owe them a stable version contract.

When To Split Back Out

Sometimes the right move is splitting a monorepo back into multiple repos. Signals:

  1. Different release cadences. The mobile app ships every two weeks; the backend ships hourly. They keep stepping on each other.
  2. Different ownership. One team rarely touches another team's package; the shared CI is the only friction.
  3. Different security models. A package needs different secret access or audit treatment.
  4. Open-source extraction. A library outgrows the monorepo and benefits from being independent.

Splitting back out is harder than splitting forward. Plan for it.

Pro Tips

  1. Start with workspaces (pnpm or Yarn) before adding Turborepo/Nx. You can usually delay the heavyweight tools.
  2. Keep shared/ minimal. A bloated shared package recouples everything.
  3. Cache aggressively. Without it, a monorepo is a productivity tax.
  4. Pin tool versions. Pnpm, Node, Turborepo — version drift in CI vs local is a debugging nightmare.
  5. Document the boundaries. Which package can import which? Lint it.

Final Tips

The shortest version: a monorepo is a coordination tool. It pays back when you have real things to coordinate — shared types, atomic refactors, multi-app releases. It costs you when you don't, and the cost looks like CI minutes, build configs, and onboarding pain.

Use one when sharing is real. Skip it when it isn't.

Good luck — and may your incremental builds stay measured in seconds 👊