Nx + Monorepo Scalability
1. Why Do We Need a Monorepo?
As an application grows, the problem is often not the number of files.
The bigger problem is managing multiple applications, libraries, and services together.
Consider a growing product:
Product
│
┌────────────────┼────────────────┐
│ │ │
Admin Web App Backend
│
┌────────────────┼────────────────┐
│ │ │
Auth Payments Products
A separate-repository approach might look like:
admin-repo
web-repo
auth-repo
payment-repo
product-repo
shared-repo
This creates clear boundaries, but shared libraries require separate versioning and cross-project changes require coordination.
A monorepo takes another approach:
Monorepo
│
┌────────────┼────────────┐
│ │ │
Apps Services Libraries
│ │ │
Web/Admin Auth/Payment UI/Utils
The projects remain separate while sharing a common workspace.
A monorepo does not automatically make the architecture scalable.
The important question is:
Can the repository grow without turning every change into a dependency and build problem?
This is where Nx becomes useful.
2. Structuring a Monorepo With Nx
Nx models a workspace as a collection of projects rather than one large application.
A typical structure might look like:
workspace/
│
├── apps/
│ ├── web/
│ ├── admin/
│ └── api-gateway/
│
├── backend-services/
│ ├── auth-service/
│ ├── payment-service/
│ ├── product-service/
│ └── email-service/
│
├── libs/
│ ├── ui/
│ ├── types/
│ ├── utils/
│ └── cache/
│
├── nx.json
├── package.json
└── pnpm-workspace.yaml
The exact folder structure is a convention. What matters is that Nx recognizes the individual projects and their tasks.
One Repository
+
Many Projects
+
Dependency-Aware Tooling
Projects can define tasks such as:
Build
Test
Lint
These may be explicitly configured or inferred by Nx plugins.
The repository provides the common source tree.
The projects provide the architectural boundaries.
Nx provides the tooling that understands their relationships.
3. The Project Graph and Task Graph
Nx builds a Project Graph that represents relationships between projects.
For example:
web
/ \
▼ ▼
ui utils
│
▼
types
You can explore it with:
nx graph
The Project Graph answers:
Which projects depend on which projects?
Nx also uses a Task Graph to determine how work should execute.
For example:
shared-ui:build
│
▼
feature-products:build
│
▼
web:build
A task pipeline can express these dependencies:
{
"targetDefaults": {
"build": {
"dependsOn": ["^build"]
}
}
}
So the distinction is:
Project Graph
│
└── Project relationships
Task Graph
│
└── Task execution
The Project Graph describes the architecture.
The Task Graph describes the work.
4. Run Only What Actually Changed
A common monorepo problem is running every task after every change.
Suppose a developer changes:
apps/admin
There may be no reason to rebuild or test unrelated projects.
Nx provides the affected workflow:
nx affected -t lint test build
Nx combines Git changes with the project graph to determine which projects are affected.
Conceptually:
Git Changes
│
▼
Project Graph
│
▼
Affected Projects
│
├── admin
├── ui
└── utils
Unrelated projects can be skipped.
You can also inspect the affected graph:
nx graph --affected
The workflow becomes:
Git Changes
│
▼
Determine What Is Affected
│
▼
Execute Relevant Work
5. Caching Work
Affected analysis reduces the number of projects that need attention.
Caching reduces the amount of work that actually executes.
Run Task
│
▼
Hash Inputs
│
▼
Cache Lookup
│
┌─┴───┐
Hit Miss
│ │
▼ ▼
Replay Run
When a task is run with matching inputs, Nx can restore the previous result instead of executing it again.
For example:
{
"targetDefaults": {
"build": { "cache": true },
"test": { "cache": true },
"lint": { "cache": true }
}
}
Now:
nx build web
can reuse a matching cached result.
Affected analysis and caching solve different problems:
Affected Analysis
│
└── Which projects need attention?
Caching
│
└── Does the task need to run again?
Together:
Git Change
│
▼
Affected Analysis
│
▼
Task Graph
│
▼
Cache Lookup
│
┌──┴───┐
Hit Miss
│ │
▼ ▼
Replay Run
Remote caching extends this benefit across CI environments.
6. Dependency Boundaries
A monorepo makes sharing code easier.
That is also where coupling can grow.
Without boundaries:
Admin ─────► Web
│ │
▼ ▼
Utils ◄──── Products
│
└──────► Auth
The problem is not the monorepo itself.
The problem is uncontrolled dependencies.
Nx can enforce dependency rules through module-boundary tooling.
Projects can be tagged:
{
"tags": ["scope:admin"]
}
and:
{
"tags": ["scope:shared"]
}
A rule can then restrict what admin is allowed to depend on:
depConstraints: [
{
sourceTag: "scope:admin",
onlyDependOnLibsWithTags: [
"scope:admin",
"scope:shared",
],
},
]
This allows:
admin
│
├── admin
└── shared
while preventing:
admin
│
└── web ✗
The goal is:
Easy Sharing
+
Controlled Dependencies
7. Libraries Should Have Clear Responsibilities
A common mistake is creating one large shared library:
shared/
│
├── helpers
├── components
├── types
├── API calls
├── business rules
└── random utilities
This eventually becomes difficult to reason about.
A better structure groups code around domains and responsibilities:
libs/
│
├── booking/
│ ├── feature-shell/
│ ├── data-access/
│ └── ui/
│
├── account/
│ ├── feature-profile/
│ └── data-access/
│
└── shared/
├── ui/
├── types/
└── utils/
Some code is project-specific.
Some belongs to a domain.
Some is genuinely shared.
Project-specific
│
▼
Domain-specific
│
▼
Shared
A useful rule is:
A shared library should have a clear reason to exist.
Sharing code is not automatically a reason to create a dependency.
8. Keep Applications Thin
Applications can focus on composition while reusable functionality lives in libraries.
Instead of putting everything inside:
apps/web/
│
├── components
├── business logic
├── API logic
├── state
└── utilities
the application can compose libraries:
apps/web/
│
└── Application Shell
│
├── Feature Libraries
├── Data Access
├── UI
└── Shared Code
The application becomes responsible mainly for composition and deployment.
This also gives Nx smaller, clearer project boundaries to reason about.
Different application types can still coexist:
Monorepo
│
┌─────────────┼─────────────┐
│ │ │
Frontend Backend Tooling
│ │
┌──┴──┐ ┌──┼──┐
│ │ │ │ │
Web Admin Auth Payments Products
Nx does not require every project to use the same technology or architecture.
9. Nx and CI
A traditional CI pipeline might look like:
Pull Request
│
▼
Build Everything
│
▼
Test Everything
│
▼
Lint Everything
An Nx-based pipeline can instead focus on affected projects:
Pull Request
│
▼
Determine Affected Projects
│
▼
Build / Test / Lint
│
▼
Use Cache Where Available
For example:
nx affected -t lint test build
Remote caching allows CI to reuse matching task results.
For larger workloads, Nx Agents can distribute tasks across multiple machines:
CI
│
Task Graph
│
┌─────────┼─────────┐
│ │ │
Agent 1 Agent 2 Agent 3
│ │ │
└─────────┼─────────┘
│
Remote Cache
The modern CI workflow therefore combines:
Affected Analysis
+
Remote Caching
+
Distributed Execution
10. The Bigger Picture
Nx is not valuable simply because it keeps multiple projects in one repository.
The larger benefit is making the repository aware of its structure and the work that structure implies.
Without dependency awareness:
Monorepo
│
└── Run Everything
With Nx:
Monorepo
│
▼
Project Graph
│
├── Task Graph
├── Affected Analysis
├── Caching
└── Dependency Boundaries
A change can move through the system like this:
What changed?
│
▼
What is affected?
│
▼
What tasks are required?
│
▼
What can be restored from cache?
That is the real scalability benefit.
A scalable monorepo is not one where everything is connected. It is one where the connections are explicit, controlled, and understood by the tooling.