Turborepo - Monorepo Architecture Expert
When to Use
Load this skill when the user request matches the frontmatter description for Turborepo - Monorepo Architecture Expert.
Assumption: You know turbo run build. This covers architectural decisions.
Arguments
$ARGUMENTS: Monorepo decision, package boundary, or cache issue to analyze- Example:
/turborepo why is turbo cache missing in CI - Example:
/turborepo should packages/ui be split from packages/web-core - If empty: ask which Turborepo architecture problem is in scope
- Example:
Before Adopting Turborepo: Strategic Assessment
| Signal | Recommendation |
|---|---|
| 1-3 engineers | Polyrepo β monorepo overhead not worth it |
| <20% shared code | Polyrepo |
| >50% shared code + frequent coordination | Monorepo compelling |
| Mixed languages (Go/Python/JS) | Nx or polyrepo β Turborepo is JS/TS focused |
| All builds <5min total | Overhead not justified yet |
| Breaking changes require 3+ repos | Monorepo wins |
| Services deploy independently | Polyrepo |
Break-even: Monorepo worth it when 3+ apps share 30%+ code AND frequent coordination is required.
Critical Rule: Package Tasks, Not Root Tasks
The #1 Turborepo mistake: Putting task logic in root package.json.
// WRONG - defeats parallelization
// Root package.json
{ "scripts": { "build": "cd apps/web && next build && cd ../api && tsc" } }
// CORRECT - each package owns its task
// apps/web/package.json
{ "scripts": { "build": "next build" } }
// Root package.json - ONLY delegates
{ "scripts": { "build": "turbo run build" } }Why: Turborepo can't parallelize sequential shell commands. Package tasks enable task graph parallelization.
Decision: When to Split a Package
Considering splitting code into a package?
β
ββ Used by 1 app only β DON'T split yet
β ββ Keep in app; wait for second consumer
β WHY: Premature abstraction, overhead > benefit
β
ββ Used by 2+ apps β MAYBE split
β ββ Stable API (rarely changes) β Split
β ββ Unstable (changes every sprint) β DON'T split yet
β ββ Mixed team ownership β DON'T split (use import path)
β WHY: Shared packages need stable APIs + clear owners
β
ββ Publishing to npm β MUST split
β
ββ CI builds > 10min β Split by stability, not domain
ββ Stable packages cache; unstable packages always rebuildAnti-pattern: Creating packages for "clean architecture" with no consumers. Every package adds build, test, and version overhead.
Anti-Patterns
β #1: Circular Dependencies
Symptom: turbo run build fails with "Could not resolve dependency graph"
packages/ui β packages/utils
packages/utils β packages/ui // circularFix: Extract shared code to a third package (packages/shared).
For indirect cycles (A β B β C β A), use: npx madge --circular --extensions ts,tsx packages/
β #2: Overly Granular Packages
Symptom: Every feature touches 5+ packages; 10+ version bumps per sprint; pnpm workspace:* version hell.
Fix: Group by change frequency, not by domain:
packages/ui/ # All components (changes often)
packages/ui-primitives/ # Headless components (stable)
packages/icons/ # Generated SVGs (rarely changes)Rule: Package boundary = different change frequency. Packages that always change together should be one package.
β #3: Missing Task Dependencies
Symptom: Tests pass locally, fail in CI with "Cannot find module './dist/index.js'"
Cause: Tests run before build completes β race condition.
// WRONG - no dependsOn for test
{ "tasks": { "build": { "outputs": ["dist/**"] }, "test": {} } }
// CORRECT
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"test": { "dependsOn": ["build"] }
}
}^build = build this package's dependencies first. build = build this package first.
β #4: Cache Miss Hell
Symptom: Cache never hits; every run rebuilds everything.
Cause: inputs glob too broad β comment changes trigger rebuild.
// WRONG
{ "build": { "inputs": ["src/**"] } }
// CORRECT
{ "build": { "inputs": ["src/**/*.{ts,tsx}", "!src/**/*.test.ts"] } }Debug:
turbo run build --dry --graph # Visualize task graph
turbo run build --dry=json | jq '.tasks[] | select(.cache.status == "MISS")'Decision: Monorepo vs Polyrepo
Starting new project?
β
ββ Single team, single product β Polyrepo (simpler)
β
ββ Shared UI library β Monorepo
β ββ Develop library + test in consumers simultaneously
β
ββ Microservices in different languages β Polyrepo
β ββ Turborepo is JS/TS focused
β
ββ Multiple teams, shared code, atomic changes needed β MonorepoPractical advice: Start polyrepo, migrate to monorepo when the cross-repo coordination pain exceeds the tooling cost.
Package Boundary Patterns
By stability (recommended):
packages/core/ # Changes quarterly (semantic versioning)
packages/features/ # Changes weekly (workspace protocol)
packages/utils/ # Changes monthlyBy consumer:
packages/public-api/ # External consumers β strict versioning
packages/internal/ # Internal apps β workspace protocol OKBy team: Only works if teams rarely share code. Otherwise creates silos.
Turborepo vs Alternatives
| Prefer Turborepo | Prefer Nx | Prefer Rush |
|---|---|---|
| JS/TS monorepo | Project graph visualization needed | 100+ packages |
| Vercel remote caching | Polyglot (JS + Python + Go) | Publishing to npm is primary goal |
| pnpm/npm workspaces | Want opinionated project structure | Phantom dependency detection needed |
Error Recovery
Cache never hits
turbo run build --dry=json | jq '.tasks[0].hash'β see current hash- Narrow
inputsglob to exclude non-code files - Fallback:
"cache": falsein turbo.json temporarily to debug without cache pressure
Circular dependency error
turbo run build --dry --graph=graph.htmlβ visualize in browsernpx madge --circular --extensions ts,tsx packages/β for indirect cycles- Extract common code to
packages/shared
Tests fail in CI but pass locally
turbo run test --dry --graphβ verify build runs before test- Add
"dependsOn": ["build"]to test task turbo run test --forceβ bypass cache to confirm ordering
Overly granular packages causing version hell
git log --oneline --since="1 month ago" -- packages/β count version bumps per package- Packages that change together 5+ times β merge them
- Fallback: use
workspace:*to auto-link versions while planning merge
When to Load Full Reference
READ references/cli-options.md when: encountering 3+ unknown CLI flags, need advanced --filter patterns across 10+ packages, or setting up complex pipeline options.
READ references/remote-cache-setup.md when: setting up remote cache for teams, debugging cache auth errors, or configuring self-hosted cache with custom storage.
Do NOT load references for: basic architecture decisions, single cache miss debugging, or monorepo adoption decisions β all covered above.
Resources
- Official Docs: https://turbo.build/repo/docs