A monorepo consolidates multiple packages—such as web applications, server APIs, and shared utility libraries—within a single code repository. Without specialized management tools, continuous integration pipelines must rebuild every package on every commit. This requirement introduces high compile latencies and forces developers to run manual builds in topological dependency order. Monorepo tools like Turborepo, Nx, and Lerna address these challenges through build optimization, caching, and task scheduling.
The Caching Engine: How Task Hashes are Computed
To skip building unchanged packages, monorepo engines use input hashing. When you invoke a task (for example, turbo run build), the build runner calculates a cryptographic hash signature representing the task’s full input state:
- Source File Contents: The contents of all source files matched by the task’s glob configuration.
- Environment Variables: The values of environment variables declared in the task configuration (such as
NODE_ENVor API endpoints). - Lockfile Hashes: The dependency resolution map matching the package’s declared node modules.
- Configuration Files: Global configuration states (such as
tsconfig.base.jsonor ESLint rules).
Input Parameters (Source Files + Env Variables + Lockfile + Configs)
│
▼
Cryptographic Hash Map (SHA-256)
│
┌────────┴────────┐
▼ ▼
[Cache Hit] [Cache Miss]
│ │
Restore Outputs Execute Task Natively
from Store Path & Save to Store Path
If the calculated hash matches a previously compiled state, the engine skips execution. It instantly restores the target files (such as dist/ or .next/ output folders) and replay logs from the local or remote cache store.
Turborepo: Lightweight Task Orchestration
Turborepo focuses on minimal configuration. It reads a root-level turbo.json file and maps pipelines using standard npm, pnpm, or yarn workspaces.
Installation
# Add turbo devDependency to the workspace root
pnpm add turbo -w -D
Complete turbo.json Configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
".env",
"tsconfig.base.json"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "package.json", "tsconfig.json"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"cache": true
},
"dev": {
"dependsOn": ["^build"],
"persistent": true,
"cache": false
},
"test": {
"dependsOn": ["^build"],
"inputs": ["src/**/*.ts", "src/**/*.tsx", "**/*.test.ts", "vitest.config.ts"],
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"inputs": ["src/**/*.ts", "src/**/*.tsx", ".eslintrc.*"],
"outputs": [],
"cache": true
}
}
}
The "dependsOn": ["^build"] pipeline rule ensures that all dependencies of a package run their build commands before the package itself begins compilation.
Nx: Plugable Graph and Module Isolation
Nx provides a larger framework supporting modular code generators, dependency validation rules, and multi-language compilation targets (such as combining Go, Python, and TypeScript within a single monorepo).
Complete nx.json Configuration
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/*.spec.ts",
"!{projectRoot}/jest.config.ts"
],
"sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"]
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production"],
"cache": true
}
},
"defaultBase": "main"
}
Nx’s affected command detects modified files since a target git ref (like main) and executes tasks exclusively for changed packages and their downstream dependents.
# Execute tests only for packages affected by modifications
npx nx affected -t test
Lerna: Release Management for Package Publishing
Lerna is designed for monorepos that publish packages to public or private npm registries. While it delegates task execution pipelines to the Nx engine, it retains native commands to coordinate version bumping, changelog compilation, and npm publishing workflows.
Complete lerna.json Configuration
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent",
"npmClient": "pnpm",
"packages": ["packages/*"],
"command": {
"version": {
"conventionalCommits": true,
"changelogPreset": "angular",
"createRelease": "github"
},
"publish": {
"conventionalCommits": true,
"message": "chore(release): publish packages"
}
}
}
Publish changes across packages in a single command:
# Version and publish packages using Conventional Commit rules
npx lerna publish
Monorepo Platform Comparison
| Comparison Metric | Turborepo | Nx | Lerna |
|---|---|---|---|
| Orchestration Language | Go | TypeScript | TypeScript (via Nx core) |
| Configuration Style | Declarative (turbo.json) | Programmatic / Config-heavy | Publishing-focused |
| Remote Cache Support | Yes (Vercel or custom API) | Yes (Nx Cloud or custom) | Yes (inherits Nx features) |
| Code Generators | Minimal generators | Advanced generators | None |
| Release Management | Requires external tooling | Requires external plugins | Built-in publish workflows |
Advanced Dependency Management in Monorepos
Maintaining dependency version consistency across a monorepo with dozens of packages is challenging. If @myorg/ui uses react version 18.2.0 and @myorg/dashboard uses react version 18.3.1, it causes version mismatch bugs and duplicate packages in your production bundles.
To address this, developers use two primary approaches:
- Syncpack: A tool that scans all
package.jsonfiles in your monorepo, flags mismatched dependencies, and automatically updates them to consistent versions:npx syncpack list-mismatches npx syncpack fix-mismatches - pnpm Catalogs: In pnpm v9+, you can define dependency versions in a central location (
pnpm-workspace.yaml) and reference them using thecatalog:protocol in individual packages, ensuring consistency:# pnpm-workspace.yaml catalog: react: ^18.3.1 typescript: ^5.5.4// packages/ui/package.json "dependencies": { "react": "catalog:" }
What Breaks in Production: Failure Modes and Mitigations
Managing multi-package systems with build caching introduces operational edge-cases.
1. Stale Cache Hits from Missing Environment Variable Declarations
Build scripts for web frameworks (like Next.js or Vite) often embed environment variables (e.g., NEXT_PUBLIC_API_URL) into compiled assets.
- Failure Mode: If a developer changes
NEXT_PUBLIC_API_URLon a CI agent, but the variable is omitted from theglobalDependenciesortasks.build.envarrays inturbo.json, the task hash remains identical. The runner returns a stale cache hit containing the old API URL, causing runtime errors in production. - Mitigation: Declare all variables that affect build output in the task’s
envconfiguration:"build": { "env": ["NEXT_PUBLIC_API_URL", "DATABASE_URL"] }
2. Phantom Dependency Resolution Failures
Monorepos use package-manager workspaces to link local libraries. In pnpm or yarn workspaces, a package might import a module (such as lodash) that is declared in the root package.json but missing from its own package.json.
- Failure Mode: The build succeeds on developer machines because the package can resolve the module from the root
node_modulesfolder. However, the build crashes inside isolated Docker builds or serverless deployments because the output artifact lacks the undeclared dependency. - Mitigation: Use tools like
syncpackor configure ESLint rules (such asimport/no-extraneous-dependencies) to verify that all imported packages are explicitly declared in each package’s localpackage.json.
3. Remote Cache Pollution in Branch Pipelines
When using remote caching, multiple CI runners share a central build asset store.
- Failure Mode: If a branch build contains experimental code changes, and the task runner writes this output to the remote cache under a shared hash signature, other branches or the main deployment pipeline can pull this experimental build. This pollutes staging or production builds with incomplete code.
- Mitigation: Configure remote caching scopes using target branch refs as namespace prefixes. Ensure that write access to the main branch cache is restricted to release pipelines.
4. Circular Local Package Dependencies
Monorepos organize code by splitting features into local packages that depend on each other (e.g., @myorg/ui importing @myorg/theme).
- Failure Mode: If developer updates cause
@myorg/themeto import a component from@myorg/ui, it creates a circular dependency. The graph resolver enters an infinite loop, crashing with a stack overflow or failing to establish a topological build order. - Mitigation: Enforce directional boundary rules. Use ESLint rules (like Nx’s dependency constraints) to prevent packages from importing modules from downstream dependencies.
Frequently Asked Questions
How do I configure Turborepo to cache environment variables used in build scripts?
Declare the environment variables inside the env array of the build task in your turbo.json file. This tells Turborepo to include the values of these variables in the input hash calculation, forcing a rebuild if their values change.
What is the exact difference between topological build ordering and parallel execution?
Topological ordering executes tasks based on their place in the dependency graph, ensuring that dependencies are compiled before the packages that import them. Parallel execution runs tasks concurrently across packages without ordering constraints, which is useful for tasks like linting.
How do I clear the local Turborepo and Nx cache directories?
Delete the cache folders in your workspace. For Turborepo, remove the .turbo/cache directory. For Nx, remove the .nx/cache directory. You can also run the clean command:
# Clear local caches
rm -rf .turbo/cache .nx/cache
Can I restrict packages in my monorepo from importing specific shared libraries?
Yes. Nx supports tagging packages (e.g., type:app, type:lib) and defining dependency constraints in nx.json. Combined with ESLint, this enforces boundaries that block incorrect imports (such as database modules being imported into frontend UI packages).
How do I configure a custom remote cache server for Turborepo?
You can set up an open-source server like turborepo-remote-cache and configure the environment variables in your local development terminals or CI runner systems:
export TURBO_API="https://your-remote-cache-url.com"
export TURBO_TOKEN="your-secure-access-token"
Once configured, running turbo build automatically communicates with your custom server instead of Vercel’s endpoints.
What is the difference between independent and fixed version modes in Lerna?
In fixed mode, all packages in the monorepo share a single version number (e.g., 1.2.0). Bumping any package bumps all packages. In independent mode, packages are versioned separately based on their changes, allowing packages to update from 1.0.0 to 1.0.1 while others remain on 2.4.0.
Wrapping Up
For most TypeScript-based projects, Turborepo provides a lightweight task runner that is easy to configure. Nx offers a full-featured workspace framework, making it a good choice for large, multi-language monorepos. Lerna is designed to automate versioning and release workflows for package publishers.