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.

published: reading time: 20 min read author: Geek Workbench updated: March 31, 2026
Quick Summary

Nx and Turborepo leverage Git to compute which packages actually changed, then build only those plus their dependents—no rebuilding everything. Remote caching makes CI dramatically faster by sharing build artifacts across machines. The critical setting: fetch-depth: 0 in CI so the tools can see full history. Pick Nx for polyglot enterprise repos, Turborepo for JS/TS simplicity.

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

ScenarioImpactMitigation
Cache corruptionWrong results servedClear cache; use cache versioning
Dependency graph incorrectMissed affected packagesRun nx reset or turbo prune
Remote cache unavailableFalls back to localConfigure local cache fallback
Large monorepo performanceSlow graph computationUse sparse checkout; optimize workspace
CI fetch-depth too shallowCan’t compute affectedUse fetch-depth: 0 in CI

Trade-off Analysis

AspectNxTurborepo
Learning curveSteeperGentler
Plugin ecosystemRichGrowing
Language supportMulti-languagePrimarily JS/TS
CachingNx CloudVercel/Self-hosted
ConfigurationMore complexSimpler
Best forEnterprise monoreposJS/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 Considerations

  • 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-PatternWhy It’s BadFix
Not configuring cache outputsCache misses on every runDefine outputs in turbo.json/nx.json
Ignoring dependency graphWrong affected computationValidate graph regularly
Shallow clone in CICan’t compute affectedUse fetch-depth: 0
No cache eviction policyCache grows indefinitelySet cache limits and TTL
Mixing package managersInconsistent resolutionUse 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

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.

Why the graph goes stale: Both Nx and Turborepo build their dependency graphs from project.json/workspace.json and package.json declarations. They do not scan import statements at runtime. If a developer imports a file without adding it to dependencies, the graph has no way to know about that relationship. This is the most common root cause behind incorrect affected calculations.

Concrete example of the failure chain:

# Developer adds import but forgets to declare dependency
# packages/utils/src/helpers.ts
export function formatDate(date: Date): string { ... }

# packages/web/src/components/DateDisplay.tsx
import { formatDate } from '@company/utils/src/helpers'; # WRONG — uses internal path
# package.json in packages/web does NOT list @company/utils as dependency

Nx/Turborepo never sees this import because it is not a package-level declaration. When the developer runs nx affected --base=main, packages/web does not appear in the affected set, even though formatDate changed.

How to detect this before it reaches CI:

# Nx: visualize the actual project graph
npx nx graph

# Turborepo: dry-run shows what would execute
turbo run build --dry-run

# Check for undeclared dependencies with madge
npx madge --circular --extensions ts packages/web/src

Mitigation: Add a CI step that statically analyzes import statements and compares them against declared package dependencies. Tools like dependency-cruiser or madge do this. Fail the build if actual imports go beyond what package.json declares. For Nx, run nx reset to clear the cached graph whenever project structure changes. For Turborepo, use turbo run build --force to bypass the cache and force a fresh graph computation when in doubt. On top of that, enforce import boundaries via ESLint rules like import/no-unresolved to catch cross-package imports that bypass package boundaries.

Extended Trade-offs

AspectNxTurborepoLernaBazel
Build speedFast with cachingVery fast — Rust coreSlow — no native cachingFastest — distributed
Language supportMulti-language (JS, Python, Go, etc.)Primarily JS/TSJS/TS onlyAny language
ComplexityHigh — many conceptsLow — simple pipeline configMedium — legacy toolingVery high — steep learning curve
EcosystemRich plugin systemGrowing, Vercel-backedDeclining — superseded by TurborepoEnterprise-grade
Best forLarge enterprise monoreposJS/TS teams wanting simplicityLegacy projectsPolyglot, massive scale

Extended Observability Checklist

Monorepo Git Metrics

A monorepo that scales gracefully and one that gradually turns into an unmaintainable tangle often look identical in the first six months. The difference shows up in what you measure. These seven metrics give you early warning signals before problems become crises.

Affected scope — Number of packages affected per PR. This is your most direct measure of coupling. Track the rolling average over the last 20 PRs. A sudden jump from a baseline of 3-5 packages to 15+ packages in a single PR is a red flag. It usually means someone accidentally changed a widely-imported file or introduced a new cross-package dependency. Set a threshold alert: if a PR affects more than 20% of the workspace, require a second reviewer.

Build cache hit rate — Percentage of builds served from cache. Target: above 70% for local builds, above 60% for CI. Below 50% means your cache keys are unstable or your inputs are too broad. Common causes: including package-lock.json in inputs (it changes on every npm install), environment variables that vary per run, or timestamps in source files. Run turbo run build --dry-run to inspect which inputs are contributing to the hash.

PR size vs. affected count — Calculate affected_packages / changed_files. In a well-structured monorepo this ratio is close to 1. A ratio above 2 means a single file change is cascading through many packages, which signals tight coupling. Watch this ratio over time; a gradually increasing trend means the dependency graph is getting deeper without anyone intervening.

Dependency graph depth — The longest chain from leaf package to root app. Measure it with nx graph --output=json | jq '[.nodes[].data.implicitDeps] | max_by(length)' or the Turborepo visualizer. Depth above 5 is a warning sign; above 8 is critical. Deep chains amplify the blast radius of every change. If depth is growing, consider extracting shared utilities into flatter intermediate packages rather than letting every app import from a single monolithic packages/core.

Build time per package — Identify your top5 slowest packages by average build time. These become CI bottlenecks. For each slow package, check whether it is doing work that could be cached or whether it has unnecessary build dependencies. A package that takes more than 5 minutes to build should get its own dedicated CI runner in a parallel job matrix.

Cache storage growth — Remote caches grow unbounded without policies. Monitor growth weekly. For Turborepo, set a cache size limit with --remote-cache-size=10gb. For Nx Cloud, configure retention policies. If your cache grows more than 20% week-over-week without adding new packages, you probably have artifact bloat from packages that declare too many output globs.

Cross-package coupling — Measure how often changes in one package force rebuilds in others. One practical approach: label every package pair that appears together in affected sets over a 30-day window. If package A and package B always appear together, they are effectively one unit. Consider merging them. High coupling across many packages is a reliable indicator that your monorepo is starting to behave like a monolith — and monoliths eventually lose the ability to do targeted affected-based builds.

Cross-Roadmap References

Interview Questions

1. How do Nx and Turborepo determine which packages are affected?

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.

2. What's the difference between Nx and Turborepo?

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.

3. Why is 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.

4. How does remote caching work in monorepo tools?

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.

5. How do you handle versioning in a monorepo?

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.

6. What strategies exist for optimizing monorepo build performance at scale?

Key strategies include: sparse checkout (only fetch needed packages), module federation for shared dependencies, build caching at both local and remote levels, affected-based execution to skip unchanged packages, parallel execution with proper task orchestration, output caching to reuse build artifacts, and dependency minimization to reduce the graph size. For extreme scale, consider Bazel with its distributed caching and remote execution capabilities.

7. How do you debug a stale dependency graph in monorepo tools?

Debug by: running nx graph or turbo run build --dry-run to visualize the actual graph, checking project.json/package.json for missing dependencies, verifying all imports are declared in package.json, running nx reset or clearing turbo cache to force recalculation, and adding CI validation that compares declared dependencies against actual imports using tools like dependency-cruiser or madge.

8. What are the security implications of remote caching in monorepos?

Security concerns include: artifact poisoning (malicious cache entries), cache leakage (sensitive data in build artifacts), token scope (overprivileged cache access tokens), and cache expiration (stale artifacts with vulnerabilities). Mitigations: use signed cache artifacts, encrypt cache at rest and in transit, scope tokens to read/write specific artifacts, implement cache eviction policies, and audit cache contents regularly for secrets or vulnerabilities.

9. How does Turborepo's Rust-based core differ from Nx's TypeScript implementation in practice?

Turborepo's Rust core provides faster task scheduling and graph traversal, better memory efficiency for large workspaces, and native parallelism. Nx's TypeScript core offers richer plugin ecosystem, generators and code generation, and deeper IDE integration. For small-to-medium JS/TS monorepos, Turborepo's speed is advantageous. For enterprise polyglot monorepos with complex needs, Nx's plugin system and generators provide more value.

10. Describe the trade-offs between local caching and remote caching for monorepo CI.

Local caching: Fast for repeated builds on same machine, no network overhead, limited sharing across team (each developer maintains own cache), cache size limited by disk space. Remote caching: Shares across team and CI runners, significant CI time savings, requires network access and authentication, potential data leakage concerns, needs cache invalidation strategy. Best practice: use local as fallback when remote unavailable, configure cache size limits, implement TTL-based eviction.

11. How do monorepo tools handle circular dependencies between packages?

Detection: Both Nx and Turborepo will fail or warn when cycles are detected in the dependency graph. Nx provides nx graph to visualize cycles; Turborepo errors during task scheduling. Resolution: refactor to extract shared code into a new package both depend on, merge competing packages if they're tightly coupled, or use peer dependencies strategically. Prevention: enforce dependency rules via ESLint plugins (e.g., import/order), use dependency-cruiser in CI, establish package boundary conventions.

12. What strategies help reduce the blast radius of changes in a large monorepo?

Strategies include: enforce acyclic dependencies between packages, use plugin architecture to isolate core from extensions, implement package boundaries with strict import rules, adopt incremental migration rather than bulk refactoring, leverage affected builds to limit CI scope, design stable API boundaries between packages (avoid internal exposure), use feature flags for cross-cutting changes, and measure coupling metrics to detect growing dependencies.

13. How do you migrate an existing multi-package repository to use monorepo tooling?

Migration steps: 1) Document current package structure and dependencies, 2) Choose tooling (Nx vs Turborepo based on needs), 3) Set up workspace configuration with minimal changes, 4) Configure affected builds with wide base (build all initially), 5) Set up remote caching, 6) Add CI pipeline with affected detection, 7) Incrementally tighten the affected base from main to feature branches. Tools like nx init or turbo init provide migration assistants.

14. How does monorepo caching interact with environment-specific builds?

Environment differences (dev/staging/prod) create different input hashes, meaning caches are not directly shared across environments. Strategies: include environment as part of cache key (e.g., turbo run build --env=production), use environment-agnostic base images for caching, leverage layer caching in Docker with proper buildKit configuration, and separate configuration from artifacts so build caches work across environments. Nx Cloud and Turborepo both support environment-aware hashing.

15. What role does GitOps play in monorepo build orchestration?

GitOps provides the single source of truth for what changed and when. Monorepo tools leverage Git metadata: git diff --name-only for file changes, git log for history traversal, branch comparisons for affected sets, and commit SHAs for cache keys. GitOps workflows trigger monorepo builds via webhooks, use branch protection for validation, and ensure that every change is auditable through Git history. The affected computation is essentially a GitOps operation applied to the dependency graph.

16. How do you handle monorepo tool configuration drift across teams?

Mitigation strategies: centralized configuration in root-level nx.json/turbo.json with strict linting, preset configurations distributed as packages (e.g., @company/eslint-config), CI validation that rejects non-conforming configs, codegen templates via Nx generators for consistent project setup, configuration as code checked into repo with PR review requirements, and automated updates via migration scripts when tool versions change.

17. What metrics should teams track to measure monorepo health?

Key metrics: affected package ratio (packages changed per total), cache hit rate (local and remote), build time per package (identify bottlenecks), dependency graph depth (longest chain), cross-package coupling (how often packages affect each other), PR size vs. affected scope correlation, time to first meaningful build, and cache storage growth rate. Dashboard these in CI and review trends weekly to catch architectural degradation early.

18. How do monorepo tools compare in handling monolithic versus modular application structures?

For modular architectures (well-defined packages with clear boundaries), both Nx and Turborepo excel at affected-based builds and caching. For monolithic apps (single large codebase split artificially), tools struggle because the dependency graph is shallow and almost everything is "affected" on each change. Recommendation: if monolith is too large, split into meaningful packages with explicit dependencies rather than artificially folder-structured packages. The tools work best when there's a genuine dependency graph to traverse.

19. What strategies exist for handling different package publish cadences in a monorepo?

Strategies: fixed versioning (all packages release together, simpler but less flexible), independent versioning (each package versions via Changesets/Lerna, more complex but targeted), release channels (canary/beta/stable per package), delayed publishing (staging area with batched releases), and aggregation releases (combine multiple package changes into single release). Choose based on coupling: highly coupled packages benefit from fixed versioning; independent packages suit independent versioning.

20. How does affected-based rebuilding interact with database migrations?

Database migrations are stateful operations that don't fit cleanly into file-based change detection. Strategies: mark migrations as always-affected for their package, use separate migration pipelines that run unconditionally when schema changes are detected, configure explicit dependencies between app packages and migration packages, and ensure migrations run before dependent apps via proper task ordering. Consider using tags like nx-ignore for non-code changes or implementing a migration flag system in your CI.

Further Reading

Conclusion

Monorepos push Git to its limits with thousands of files and hundreds of packages. Tools like Nx and Turborepo leverage Git’s change detection to run only the affected tasks — they don’t replace Git but layer on top of it, making large repositories as fast to work with as small ones.

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.

#git #version-control #submodules

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.

#git #version-control #svn

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.

#git #version-control #changelog