Monorepo Management
Monorepo strategies with Nx and Turborepo for scalable multi-project repositories
You are an expert in monorepo strategies using tools like Nx and Turborepo for scalable multi-project repositories. ## Key Points - Atomic cross-project changes in a single commit - Shared tooling, lint rules, and CI configuration - Simplified dependency management between internal packages - Enable remote caching (Nx Cloud or Turborepo Remote Cache) so CI and teammates reuse build artifacts instead of rebuilding unchanged projects - Define clear ownership with a CODEOWNERS file mapping each `apps/` and `packages/` directory to the responsible team - Use affected commands in CI to avoid running the entire test suite on every pull request - Not fetching full git history in CI (`fetch-depth: 0`), which prevents affected-change detection from finding the merge base - Allowing circular dependencies between packages, which breaks incremental builds and caching ## Quick Example ```bash # Nx — only test projects affected by changes since main npx nx affected -t test --base=main # Turborepo — run tests with dependency-aware filtering npx turbo run test --filter=...[origin/main] ``` ```bash npx create-turbo@latest ```
skilldb get git-workflow-skills/Monorepo ManagementFull skill: 145 linesMonorepo Management — Git Workflows
You are an expert in monorepo strategies using tools like Nx and Turborepo for scalable multi-project repositories.
Overview
A monorepo is a single repository containing multiple distinct projects or packages. Tools like Nx and Turborepo add dependency-aware task execution, remote caching, and affected-change detection to keep builds fast as the repository grows.
Core Concepts
Why monorepos:
- Atomic cross-project changes in a single commit
- Shared tooling, lint rules, and CI configuration
- Simplified dependency management between internal packages
Workspace structure (typical):
repo/
apps/
web/
api/
packages/
ui/
utils/
config-eslint/
package.json # root workspace config
nx.json / turbo.json # orchestration config
Affected-change detection:
# Nx — only test projects affected by changes since main
npx nx affected -t test --base=main
# Turborepo — run tests with dependency-aware filtering
npx turbo run test --filter=...[origin/main]
Implementation Patterns
Nx setup:
npx create-nx-workspace@latest myorg --preset=ts
# Add a project
npx nx generate @nx/node:application api
# Run a target
npx nx run api:build
# Visualize the dependency graph
npx nx graph
// nx.json
{
"targetDefaults": {
"build": { "dependsOn": ["^build"], "cache": true },
"test": { "cache": true }
},
"namedInputs": {
"default": ["{projectRoot}/**/*"],
"production": ["default", "!{projectRoot}/**/*.spec.ts"]
}
}
Turborepo setup:
npx create-turbo@latest
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
}
}
}
CI with affected detection (GitHub Actions):
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- run: npm ci
- run: npx nx affected -t lint test build --base=origin/main
Core Philosophy
A monorepo is a bet that the benefits of atomic cross-project changes, shared tooling, and unified CI outweigh the costs of increased repository size, more complex build orchestration, and tighter coupling between teams. This bet pays off when projects in the repository genuinely share dependencies, APIs, or release cycles. It fails when unrelated projects are colocated simply because "we have a monorepo" — adding build complexity and coupling without the cross-project coordination benefits that justify it. The decision to monorepo should be driven by actual dependency relationships, not organizational convention.
Build performance in a monorepo is solved by two complementary techniques: affected-change detection and remote caching. Affected detection (Nx's affected, Turborepo's --filter) answers "which projects could possibly be impacted by this change?" and runs only those tasks. Remote caching answers "has someone already run this exact task with this exact input?" and reuses the output. Together, they make CI times proportional to the scope of the change rather than the size of the repository. Without both, monorepo CI times grow linearly with the number of projects and eventually become a bottleneck that negates the workflow benefits.
The dependency graph is the monorepo's most important artifact. Tools like Nx and Turborepo build a graph of which packages depend on which other packages and use it to determine task ordering, parallelization, and affected-change detection. This graph must be accurate — a missing dependency edge means a package is not rebuilt when its dependency changes, producing silent staleness. A circular dependency breaks the graph entirely and prevents incremental builds. Maintaining the dependency graph's integrity (no cycles, no missing edges) is a continuous discipline, not a one-time setup.
Anti-Patterns
-
Running all tasks on every change. Not using affected-change detection means every PR triggers builds and tests for every project in the repository, regardless of what changed. This wastes CI compute and makes PR feedback cycles unnecessarily slow. Use
nx affectedorturbo --filter=...[base]in CI. -
Shallow clones in CI without base ref. Using
fetch-depth: 1in GitHub Actions prevents affected-change detection from finding the merge base with the main branch. Without the full history between the PR branch and main,affectedcannot determine what changed and falls back to running everything. Usefetch-depth: 0or fetch enough history to include the merge base. -
Circular dependencies between packages. Package A depending on Package B which depends back on Package A breaks the dependency graph, prevents incremental builds, and makes task ordering impossible. Design packages with clear layering: shared utilities at the bottom, domain packages in the middle, applications at the top.
-
No CODEOWNERS for package directories. In a monorepo with multiple teams, changes to shared packages can merge without review from the teams that depend on them. Use a CODEOWNERS file that maps each
packages/andapps/directory to the responsible team so changes require approval from the right people. -
Local-only caching. Running Nx or Turborepo without remote caching means every CI run rebuilds from scratch even when the same task with the same inputs has already been computed by another developer or CI run. Enable remote caching so the work done by one person benefits everyone.
Best Practices
- Enable remote caching (Nx Cloud or Turborepo Remote Cache) so CI and teammates reuse build artifacts instead of rebuilding unchanged projects
- Define clear ownership with a CODEOWNERS file mapping each
apps/andpackages/directory to the responsible team - Use affected commands in CI to avoid running the entire test suite on every pull request
Common Pitfalls
- Not fetching full git history in CI (
fetch-depth: 0), which prevents affected-change detection from finding the merge base - Allowing circular dependencies between packages, which breaks incremental builds and caching
Install this skill directly: skilldb add git-workflow-skills
Related Skills
Code Review
Code review best practices for constructive, efficient pull request reviews
Conventional Commits
Conventional Commits specification for structured, machine-readable commit messages
Git Bisect Debug
Debugging with git bisect and advanced git log techniques to pinpoint regressions
Git Hooks
Git hooks automation with Husky and lint-staged for pre-commit quality gates
Gitflow
Gitflow branching model for structured release-oriented development workflows
Release Management
Release management and tagging strategies for predictable, automated software releases