Monorepo Tools: Nx, Turborepo, and Git-Aware Workspace Management
Manage monorepos with Git using Nx, Turborepo, and workspace-aware tooling. Learn affected builds, caching strategies, and versioning for multi-package repositories.
Introduction
A monorepo puts all your packages, apps, and libraries in a single Git repository. The promise is compelling: atomic commits across packages, simplified dependency management, and unified tooling. The reality is that monorepos can become unbearably slow without the right tooling — every CI run builds everything, every test suite runs everything, and every deploy ships everything.
Enter Nx and Turborepo. These tools understand the dependency graph between your packages and use Git to determine what actually changed. Instead of rebuilding the entire repository, they compute the “affected” set — only the packages that changed or depend on changed packages. Combined with remote caching, they make monorepos faster than separate repositories.
This post covers how Nx and Turborepo integrate with Git, how affected builds work, caching strategies, and production patterns for monorepo management. Whether you’re managing 5 packages or 500, these tools are essential for monorepo success.
When to Use / When Not to Use
Use monorepo tools when:
- You have multiple packages that share dependencies
- You need atomic commits across packages
- Your CI pipeline is slow from building everything
- You want unified tooling and standards
- You need to track cross-package changes
Avoid monorepo tools when:
- You have a single package or app
- Your packages are truly independent
- Your team can’t handle the tooling complexity
- You need separate deployment pipelines per package
Core Concepts
Monorepo tools work by building a dependency graph and using Git to detect changes:
flowchart TD
A[Git Push] --> B[Detect Changed Files]
B --> C[Map Files to Packages]
C --> D[Build Dependency Graph]
D --> E[Compute Affected Set]
E --> F{Cached?}
F -->|Yes| G[Use Cache]
F -->|No| H[Build Package]
G --> I[Aggregate Results]
H --> I
I --> J[Deploy Affected]
Architecture and Flow Diagram
sequenceDiagram
participant Dev as Developer
participant Git as Git Repository
participant Tool as Nx/Turborepo
participant Graph as Dependency Graph
participant Cache as Remote Cache
participant CI as CI Pipeline
Dev->>Git: Push changes
Git->>Tool: Trigger pipeline
Tool->>Git: git diff --name-only HEAD~1
Git-->>Tool: Changed files
Tool->>Graph: Map files to packages
Graph->>Graph: Compute affected packages
Graph-->>Tool: Affected set
Tool->>Cache: Check cache for each package
Cache-->>Tool: Cache hits/misses
Tool->>CI: Build only cache misses
CI-->>Tool: Build results
Tool->>Cache: Store new results
Tool-->>Dev: Pipeline complete
Step-by-Step Guide
1. Nx Setup
# Create Nx workspace
npx create-nx-workspace@latest my-monorepo
# Or add to existing repo
npx nx@latest init
Project configuration:
// nx.json
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"cache": true
},
"test": {
"dependsOn": ["build"],
"cache": true
},
"lint": {
"cache": true
}
},
"defaultBase": "main"
}
Affected builds:
# Run tests only for affected packages
nx affected:test
# Build affected packages and their dependencies
nx affected:build
# See what would be affected
nx affected --base=main --head=HEAD
# Run with parallelism
nx affected:test --parallel=4
2. Turborepo Setup
# Initialize Turborepo
npx turbo init
# Or add to existing workspace
npm install turbo --save-dev
Pipeline configuration:
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": [],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Running tasks:
# Run build for all packages
turbo run build
# Run only for changed packages
turbo run build --filter="...[origin/main]"
# Run with concurrency
turbo run build --concurrency=4
# Dry run to see what would execute
turbo run build --dry-run
3. Git Integration Patterns
Nx with Git:
# Compare against main branch
nx affected --base=main
# Compare against last successful build
nx affected --base=HEAD~1
# Custom range
nx affected --base=v1.0.0 --head=v1.1.0
Turborepo with Git:
# Filter by changed packages since main
turbo run build --filter="...[origin/main]"
# Filter specific package
turbo run build --filter=my-package
# Filter by tag
turbo run build --filter="...[HEAD~1]"
4. Remote Caching Setup
Nx Cloud:
npx nx connect
# Sets up NX_CLOUD_ACCESS_TOKEN
Turborepo Remote Cache:
# Use Vercel Remote Cache
turbo login
turbo link
# Or self-hosted
turbo run build --remote-cache=http://cache.internal:3000
5. CI Configuration
# GitHub Actions with Nx
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx nx affected -t lint test build --parallel=3
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
Production Failure Scenarios + Mitigations
| Scenario | Impact | Mitigation |
|---|---|---|
| Cache corruption | Wrong results served | Clear cache; use cache versioning |
| Dependency graph incorrect | Missed affected packages | Run nx reset or turbo prune |
| Remote cache unavailable | Falls back to local | Configure local cache fallback |
| Large monorepo performance | Slow graph computation | Use sparse checkout; optimize workspace |
| CI fetch-depth too shallow | Can’t compute affected | Use fetch-depth: 0 in CI |
Trade-offs
| Aspect | Nx | Turborepo |
|---|---|---|
| Learning curve | Steeper | Gentler |
| Plugin ecosystem | Rich | Growing |
| Language support | Multi-language | Primarily JS/TS |
| Caching | Nx Cloud | Vercel/Self-hosted |
| Configuration | More complex | Simpler |
| Best for | Enterprise monorepos | JS/TS monorepos |
Implementation Snippets
Nx workspace with affected deployment:
# Deploy only affected apps
nx affected --target=deploy --base=main --parallel=1
Turborepo with package filtering:
# Build package and its dependencies
turbo run build --filter=my-package^...
# Build everything except specific package
turbo run build --filter=!my-package
Custom affected detection script:
#!/bin/bash
# scripts/affected-check.sh
BASE=${1:-main}
AFFECTED=$(git diff --name-only $BASE HEAD | grep -oP 'packages/[^/]+' | sort -u)
echo "Affected packages: $AFFECTED"
for pkg in $AFFECTED; do
echo "Testing $pkg..."
cd $pkg && npm test
done
Observability Checklist
- Logs: Log affected package computation and cache hit rates
- Metrics: Track build time savings from affected builds
- Alerts: Alert on cache misses and graph computation failures
- Dashboards: Monitor monorepo health and build performance
- Traces: Trace file changes through dependency graph to builds
Security/Compliance Notes
- Remote cache may store build artifacts; ensure encryption
- CI tokens for cache access should be scoped and rotated
- For regulated environments, audit cache contents
- Use workspace boundaries to isolate sensitive packages
- Version cache keys to prevent stale artifact serving
Common Pitfalls / Anti-Patterns
| Anti-Pattern | Why It’s Bad | Fix |
|---|---|---|
| Not configuring cache outputs | Cache misses on every run | Define outputs in turbo.json/nx.json |
| Ignoring dependency graph | Wrong affected computation | Validate graph regularly |
| Shallow clone in CI | Can’t compute affected | Use fetch-depth: 0 |
| No cache eviction policy | Cache grows indefinitely | Set cache limits and TTL |
| Mixing package managers | Inconsistent resolution | Use workspaces consistently |
Quick Recap Checklist
- Choose Nx or Turborepo based on needs
- Configure pipeline with proper inputs/outputs
- Set up remote caching
- Configure CI with affected builds
- Validate dependency graph accuracy
- Monitor cache hit rates
- Document workspace structure
- Set up cache eviction policies
Interview Q&A
They compare the current Git state against a base reference (usually main branch), identify changed files, map those files to packages, then traverse the dependency graph to find all packages that depend on changed ones. This ensures that if package A changes, package B (which depends on A) is also rebuilt.
Nx is a full build system with plugins, generators, and multi-language support. Turborepo is focused on task orchestration and caching for JavaScript/TypeScript workspaces. Nx is more feature-rich but complex; Turborepo is simpler and faster to adopt. Both use Git for affected detection and support remote caching.
fetch-depth: 0 critical for monorepo CI?Monorepo tools need Git history to compute affected packages. With shallow clones, they can't compare against the base branch and may either build everything or miss affected packages. fetch-depth: 0 ensures full history is available for accurate change detection.
Before building a package, the tool computes a hash from input files, dependencies, and environment. It checks the remote cache for this hash. If found, it downloads the cached artifacts instead of building. If not found, it builds and uploads the result to the cache for future use.
Use fixed versioning (all packages share one version) or independent versioning (each package versions separately). Tools like Changesets or Lerna manage independent versioning with changelog generation. For fixed versioning, semantic-release can version the entire workspace together.
Extended Architecture Diagram
flowchart TD
subgraph "Git Repository"
A[packages/ui] --> B[packages/api]
A --> C[apps/web]
B --> C
B --> D[apps/admin]
end
subgraph "Change Detection"
E[git diff --name-only] --> F{Changed Files}
F --> G[Map to packages/ui]
F --> H[Map to packages/api]
end
subgraph "Dependency Graph"
G --> I[apps/web depends on ui]
G --> J[apps/admin depends on api]
H --> J
H --> I
end
subgraph "Affected Set"
I --> K[Build ui, api, web, admin]
J --> K
end
subgraph "Cache Layer"
K --> L{Cache Hit?}
L -->|Yes| M[Download artifacts]
L -->|No| N[Build and cache]
end
Extended Production Failure Scenario
Incorrect Affected Calculation Causing Skipped Builds
A developer changes a shared utility function in packages/utils/src/helpers.ts. The monorepo tool’s dependency graph is stale because package.json wasn’t updated — the utility is imported directly without being declared as a dependency. The affected calculation misses apps/web which imports the helper. The CI skips building apps/web, the change deploys without testing, and production breaks with a runtime error.
Mitigation: Regularly validate the dependency graph: nx graph or turbo run build --dry-run. Use explicit workspace dependencies in package.json rather than implicit file imports. Add a CI check that verifies the graph matches actual imports. Run nx reset or clear the turbo cache when graph inconsistencies are suspected.
Extended Trade-offs
| Aspect | Nx | Turborepo | Lerna | Bazel |
|---|---|---|---|---|
| Build speed | Fast with caching | Very fast — Rust core | Slow — no native caching | Fastest — distributed |
| Language support | Multi-language (JS, Python, Go, etc.) | Primarily JS/TS | JS/TS only | Any language |
| Complexity | High — many concepts | Low — simple pipeline config | Medium — legacy tooling | Very high — steep learning curve |
| Ecosystem | Rich plugin system | Growing, Vercel-backed | Declining — superseded by Turborepo | Enterprise-grade |
| Best for | Large enterprise monorepos | JS/TS teams wanting simplicity | Legacy projects | Polyglot, massive scale |
Extended Observability Checklist
Monorepo Git Metrics
- Affected scope — Number of packages affected per PR. Track trends; sudden spikes indicate coupling issues.
- Build cache hit rate — Percentage of builds served from cache. Target: > 70%. Low rates indicate cache invalidation issues.
- PR size vs. affected count — Correlate changed files with affected packages. Large PRs affecting many packages increase merge risk.
- Dependency graph depth — Track the longest dependency chain. Deep chains increase blast radius for changes.
- Build time per package — Identify slow packages that bottleneck the pipeline.
- Cache storage growth — Monitor remote cache size. Set eviction policies to prevent unbounded growth.
- Cross-package coupling — Measure how often changes in one package affect others. High coupling indicates architectural issues.
Cross-Roadmap References
- System Design Learning Roadmap — Broader system architecture context for monorepo scaling decisions
- Microservices Architecture — System Design roadmap: service-level dependency patterns
Resources
Category
Related Posts
Git Submodules and Subtrees: Managing External Dependencies
Master git submodules and subtrees for including external repositories. Learn the trade-offs, synchronization workflows, dependency management patterns, and when to use each approach.
Centralized vs Distributed VCS: Architecture, Trade-offs, and When to Use Each
Compare centralized (SVN, CVS) vs distributed (Git, Mercurial) version control systems — their architectures, trade-offs, and when to use each approach.
Automated Changelog Generation: From Commit History to Release Notes
Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.