ADR-0011: Nx for Monorepo Build Orchestration
Status
Accepted - 2025-01-26
Context
TVL Platform monorepo (ADR-0010) has 10-20 packages that need to be built, tested, and linted. We need a build system that:
Business Requirements
- Fast CI/CD (< 10 minutes for full build)
- Cost-effective (minimize CI minutes)
- Developer productivity (fast local builds)
Technical Requirements
- Incremental builds (only rebuild changed packages)
- Dependency graph awareness (build in correct order)
- Caching (local and remote)
- Parallel execution (utilize all CPU cores)
- Affected detection (only test changed packages)
Constraints
- pnpm workspaces already chosen (ADR-0010)
- TypeScript 5.3+ (ADR-0005)
- GitHub Actions for CI/CD
- Must work with Docker
Decision
Nx for build orchestration, task execution, and caching in the monorepo.
Configuration
// nx.json
{
  "tasksRunnerOptions": {
    "default": {
      "runner": "nx/tasks-runners/default",
      "options": {
        "cacheableOperations": ["build", "test", "lint"],
        "parallel": 3
      }
    }
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["{projectRoot}/dist"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}
Rationale
- Incremental Builds: Only rebuild changed packages (90% faster)
- Smart Caching: Cache build outputs locally and remotely
- Affected Detection: Only test packages affected by changes
- Dependency Graph: Automatically determines build order
- Parallel Execution: Runs tasks in parallel (uses all CPU cores)
Alternatives Considered
Alternative 1: Turborepo
Rejected
Pros:
- Simple configuration
- Good caching (local and remote)
- Fast builds
Cons:
- Less mature than Nx (newer project)
- Fewer features (no dependency graph visualization)
- No affected detection (must build all)
- Acquired by Vercel (potential vendor lock-in)
Decision: Nx more mature and feature-complete.
Alternative 2: pnpm Scripts Only
Rejected
Pros:
- No additional tooling
- Simple (just pnpm)
Cons:
- No Caching: Rebuild everything every time
- No Affected Detection: Test everything
- No Parallelization: pnpm runs tasks sequentially
- Slow CI/CD: 30 minutes vs. 5 minutes with Nx
Decision: Nx provides critical performance benefits.
Alternative 3: Lerna
Rejected
Pros:
- Popular monorepo tool
- Handles versioning and publishing
Cons:
- Slower than Nx: No advanced caching
- Less Active: Maintenance has slowed
- Redundant: pnpm + Nx provide same functionality
- Heavyweight: We don't need versioning/publishing
Decision: Nx faster and more actively maintained.
Alternative 4: Bazel
Rejected
Pros:
- Google-scale monorepo (proven at massive scale)
- Extremely fast (advanced caching)
- Multi-language support
Cons:
- Steep Learning Curve: Complex configuration (Starlark language)
- Overkill: Designed for Google-scale (10,000+ developers)
- Heavyweight: Requires dedicated tooling setup
- Poor TypeScript Support: JavaScript/TypeScript not first-class
Decision: Nx designed for TypeScript monorepos, simpler.
Consequences
Positive
- 
Performance - Incremental Builds: Only rebuild changed packages
- Caching: Build once, reuse cached outputs
- Parallel Execution: Utilize all CPU cores
- CI/CD Speed: 5 minutes vs. 30 minutes without caching
 
- 
Developer Experience - Fast Feedback: Instant builds if nothing changed
- Affected Detection: Only test relevant packages
- Dependency Graph: Visualize package dependencies (nx graph)
- IDE Integration: VS Code extension available
 
- 
Cost Savings - Fewer CI Minutes: GitHub Actions charges by minute
- Example: 30 min → 5 min = 83% cost reduction
- Remote Caching: Share cache across team (Nx Cloud)
 
- 
Correctness - Dependency-Aware: Builds in correct order
- Reproducible: Deterministic builds
- Affected Detection: Reduces chance of breaking unrelated packages
 
Negative
- 
Learning Curve - Developers must learn Nx CLI
- Mitigation: CLI similar to pnpm, team adapts quickly
 
- 
Configuration Overhead - nx.json,- project.jsonfiles
- Mitigation: Nx infers configuration from package.json(minimal config)
 
- 
Nx Cloud Vendor Lock-in - Remote caching requires Nx Cloud (paid service)
- Mitigation: Local caching works without Nx Cloud (free), add remote caching later if needed
 
- 
Build Complexity - Adds another tool to the stack
- Mitigation: Nx solves real problem (slow builds), worth the complexity
 
Nx Features Used
1. Affected Detection
# Only test packages affected by changes
nx affected:test
# Only build affected packages
nx affected:build
# Only lint affected packages
nx affected:lint
How it works:
- Compares current branch to main
- Analyzes dependency graph
- Determines which packages changed
- Only runs tasks on affected packages
Example:
- Change packages/shared/utils.ts
- Nx detects: @tvl/shared,@tvl/api,@tvl/webaffected
- Only tests those 3 packages (not all 20)
2. Caching
# Run build (first time - slow)
nx build @tvl/api
# Building... (30 seconds)
# Run build again (cached - instant)
nx build @tvl/api
# Cached output retrieved (0.1 seconds)
Cache Key: Hash of source files + dependencies
Cache Location: .nx/cache/ (local)
3. Dependency Graph
# Visualize dependency graph
nx graph
Opens browser with interactive graph:
@tvl/web → @tvl/api → @tvl/database
                    → @tvl/shared
                    → @tvl/events
4. Task Orchestration
// nx.json
{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"] // Build dependencies first
    },
    "test": {
      "dependsOn": ["build"] // Build before testing
    }
  }
}
Effect: Nx builds in correct order automatically.
Common Commands
Run Tasks
# Run task in single package
nx build @tvl/api
nx test @tvl/database
nx lint @tvl/web
# Run task in all packages
nx run-many --target=build --all
nx run-many --target=test --all
# Run task in affected packages only
nx affected:build
nx affected:test
nx affected:lint
Caching
# Clear cache
nx reset
# Run without cache (force rebuild)
nx build @tvl/api --skip-nx-cache
Dependency Graph
# Open interactive graph
nx graph
# Show affected packages
nx affected:graph
CI/CD Integration
GitHub Actions
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Required for affected detection
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      - name: Build affected packages
        run: pnpm nx affected:build --base=origin/main
      - name: Test affected packages
        run: pnpm nx affected:test --base=origin/main
      - name: Lint affected packages
        run: pnpm nx affected:lint --base=origin/main
Result: Only build/test/lint packages affected by PR (fast CI).
Performance Metrics
Before Nx (pnpm only)
| Task | Time | 
|---|---|
| Build all packages | 5 minutes | 
| Test all packages | 10 minutes | 
| Lint all packages | 2 minutes | 
| Total | 17 minutes | 
After Nx (with caching)
| Task | Time (Cold) | Time (Cached) | 
|---|---|---|
| Build affected | 2 minutes | 5 seconds | 
| Test affected | 3 minutes | 10 seconds | 
| Lint affected | 30 seconds | 5 seconds | 
| Total | 5.5 minutes | 20 seconds | 
Savings: 68% faster (cold), 98% faster (cached).
Nx Cloud (Optional - Future)
Remote Caching: Share cache across team and CI/CD.
Setup
# Connect to Nx Cloud (free tier available)
npx nx connect-to-nx-cloud
Benefits
- Team shares cache (developer builds, CI builds)
- Distributed task execution (run tasks on Nx Cloud workers)
- Analytics dashboard (build times, cache hit rates)
Cost
- Free Tier: 500 hours/month (enough for MVP)
- Paid Tier: $20/user/month (if > 5 users)
Decision: Start without Nx Cloud (local caching sufficient), add later if needed.
Validation Checklist
-  nx.jsonconfigured
-  Affected detection works (nx affected:test)
- Caching enabled for build/test/lint
-  Dependency graph correct (nx graph)
- GitHub Actions uses Nx
- Local builds use cache
- Parallel execution enabled
- Team trained on Nx CLI