Monorepo 2026: Turborepo, Nx và pnpm Workspaces — Quản lý code cho team quy mô lớn
Posted on: 4/21/2026 4:10:45 AM
Table of contents
- Monorepo vs Polyrepo: Bài toán thực tế
- pnpm Workspaces — Nền tảng dependency management
- Turborepo — Task Runner tốc độ cao
- Nx — Hệ sinh thái quản lý monorepo toàn diện
- Bazel — Vũ khí của Tech Giants
- So sánh tổng thể: Turborepo vs Nx vs Bazel
- CI/CD Optimization cho Monorepo
- Code Ownership và Governance
- Best Practices cho Production Monorepo
- Migration Path: Từ Polyrepo sang Monorepo
- Kết luận
Monorepo vs Polyrepo: Bài toán thực tế
Khi dự án mở rộng từ 1 ứng dụng thành 5-10 package, câu hỏi đầu tiên luôn là: gộp hết vào một repo hay tách riêng? Monorepo đặt toàn bộ code — ứng dụng, thư viện dùng chung, config, CI/CD — vào cùng một Git repository. Polyrepo tách mỗi package thành repo riêng biệt.
Hai mô hình không tốt-xấu tuyệt đối. Polyrepo cho phép mỗi team tự chủ hoàn toàn về release cycle, nhưng đồng bộ version giữa các repo trở thành ác mộng khi số lượng package tăng. Monorepo giải quyết được dependency drift nhưng đổi lại cần tooling mạnh để giữ build nhanh và CI gọn.
graph LR
subgraph Polyrepo
R1[app-web repo]
R2[app-mobile repo]
R3[shared-ui repo]
R4[shared-utils repo]
R1 -.->|npm publish| R3
R1 -.->|npm publish| R4
R2 -.->|npm publish| R3
R2 -.->|npm publish| R4
end
subgraph Monorepo
M[Single Repository]
M --> A1[packages/app-web]
M --> A2[packages/app-mobile]
M --> A3[packages/shared-ui]
M --> A4[packages/shared-utils]
end
style R1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style R2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style R3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style R4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style M fill:#e94560,stroke:#fff,color:#fff
style A1 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style A2 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style A3 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style A4 fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
Polyrepo: mỗi package publish qua npm registry. Monorepo: link trực tiếp qua workspace protocol.
| Tiêu chí | Monorepo | Polyrepo |
|---|---|---|
| Dependency sync | Một lockfile, version luôn đồng nhất | Mỗi repo lockfile riêng, dễ drift |
| Code sharing | Import trực tiếp, không cần publish | Phải publish lên registry rồi install |
| Atomic changes | 1 PR thay đổi cả API + frontend | Phải sync nhiều PR across repos |
| CI/CD phức tạp | Cần affected detection, caching | Đơn giản: mỗi repo 1 pipeline |
| Git performance | Cần sparse checkout khi repo lớn | Mỗi repo nhẹ nhàng |
| Team autonomy | Module boundaries + CODEOWNERS | Tự chủ hoàn toàn |
pnpm Workspaces — Nền tảng dependency management
pnpm Workspaces là foundation layer cho monorepo trong hệ sinh thái JavaScript/TypeScript. Nó giải quyết bài toán cơ bản nhất: liên kết các package nội bộ với nhau mà không cần publish lên npm registry.
Khác với npm và Yarn, pnpm dùng content-addressable storage — mỗi phiên bản của một package chỉ được lưu trên đĩa đúng một lần, bất kể bao nhiêu project dùng nó. Kết hợp với symlink-based node_modules, kết quả là install nhanh hơn đáng kể và tiết kiệm dung lượng đĩa.
Cấu trúc pnpm Workspace điển hình
monorepo/
├── pnpm-workspace.yaml # Khai báo workspace packages
├── package.json # Root scripts + devDependencies
├── pnpm-lock.yaml # Single lockfile cho toàn bộ workspace
├── apps/
│ ├── web/ # Vue.js frontend
│ │ └── package.json
│ └── api/ # .NET hoặc Node.js backend
│ └── package.json
├── packages/
│ ├── ui/ # Shared UI components
│ │ └── package.json
│ ├── utils/ # Shared utilities
│ │ └── package.json
│ └── config/ # Shared ESLint, TypeScript config
│ └── package.json
└── turbo.json # Turborepo task config (optional)
File pnpm-workspace.yaml chỉ cần vài dòng:
packages:
- 'apps/*'
- 'packages/*'
Để một app dùng shared package, khai báo dependency với protocol workspace:*:
// apps/web/package.json
{
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:*"
}
}
Workspace Protocol là gì?
workspace:* báo cho pnpm biết package này nằm trong monorepo, link trực tiếp tới source code thay vì tải từ npm registry. Khi publish, pnpm tự động thay workspace:* bằng version thực tế.
pnpm Workspaces chỉ quản lý dependency — không có task orchestration, caching hay affected detection. Đó là lý do team thường kết hợp pnpm với Turborepo hoặc Nx.
Turborepo — Task Runner tốc độ cao
Turborepo (by Vercel) tập trung vào một việc duy nhất: chạy task nhanh nhất có thể thông qua caching thông minh và parallel execution. Triết lý của Turborepo là "cache outputs của scripts hiện có" — không thay đổi cách team viết code, chỉ tăng tốc quy trình build.
Content-Aware Hashing
Mỗi khi chạy task, Turborepo tính hash dựa trên: source files, dependencies, environment variables và tool versions. Nếu hash không đổi, kết quả được phục hồi từ cache trong ~50ms thay vì build lại từ đầu.
graph TD
A[turbo build] --> B{Hash đã có trong cache?}
B -->|Có| C[Restore từ cache ~50ms]
B -->|Không| D[Chạy build thực tế]
D --> E[Lưu output vào cache]
C --> F[FULL TURBO ⚡]
E --> F
style A fill:#e94560,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style C fill:#4CAF50,stroke:#fff,color:#fff
style D fill:#2c3e50,stroke:#fff,color:#fff
style E fill:#2c3e50,stroke:#fff,color:#fff
style F fill:#e94560,stroke:#fff,color:#fff
Turborepo content-aware hashing: khi hash trùng, cache hit chỉ mất ~50ms.
Task Pipeline và Topological Ordering
File turbo.json định nghĩa dependency giữa các task:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"cache": true
},
"lint": {
"cache": true
},
"dev": {
"cache": false,
"persistent": true
}
}
}
"dependsOn": ["^build"] nghĩa là: trước khi build package này, phải build xong tất cả dependencies trước. Turborepo tự động sắp xếp topological order và chạy song song các task độc lập.
Remote Caching
Local cache giúp một developer. Remote cache giúp toàn team. Khi developer A đã build xong package X, developer B clone repo và chạy build — Turborepo phát hiện hash trùng, tải kết quả từ remote cache thay vì build lại. CI cũng hưởng lợi tương tự.
# Kết nối Vercel Remote Cache
npx turbo login
npx turbo link
# Hoặc self-hosted server
# turbo.json:
{
"remoteCache": {
"signature": true
}
}
Khi nào Turborepo là lựa chọn đúng?
Team JavaScript/TypeScript 3-20 người, dưới 30 packages, muốn setup nhanh (< 30 phút) mà không cần học framework mới. Turborepo là "plug-and-play": thêm turbo.json, chạy turbo build — done.
Filter Syntax cho Selective Builds
Turborepo cho phép chọn lọc packages cần build:
# Build chỉ app-web và dependencies
turbo build --filter=@myorg/web
# Build các package thay đổi so với commit trước
turbo build --filter=...[HEAD~1]
# Build tất cả package trong thư mục apps/
turbo build --filter=./apps/*
Nx — Hệ sinh thái quản lý monorepo toàn diện
Nếu Turborepo là "task runner thuần", Nx là "build system + workspace manager". Nx không chỉ chạy task nhanh — nó hiểu toàn bộ workspace: dependency graph, module boundaries, project generators, và có thể phân tán task execution across machines.
Affected Detection — Chỉ build những gì thay đổi
Nx phân tích dependency graph ở mức file, không chỉ mức package. Khi thay đổi 1 file trong packages/utils, Nx xác định chính xác apps nào bị ảnh hưởng:
# Chỉ test các projects bị ảnh hưởng bởi changes
nx affected -t test
# Chỉ build apps bị ảnh hưởng
nx affected -t build
# So sánh với branch main
nx affected -t lint --base=main --head=HEAD
graph TD
subgraph "Dependency Graph"
UI[packages/ui]
Utils[packages/utils]
Config[packages/config]
Web[apps/web]
Mobile[apps/mobile]
Admin[apps/admin]
end
Web --> UI
Web --> Utils
Mobile --> UI
Mobile --> Utils
Admin --> UI
Admin --> Config
UI --> Utils
subgraph "Affected khi sửa Utils"
direction TB
AUtils[Utils ✏️]
AUI[UI 🔄]
AWeb[Web 🔄]
AMobile[Mobile 🔄]
AAdmin[Admin 🔄]
end
style UI fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style Utils fill:#e94560,stroke:#fff,color:#fff
style Config fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style Web fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style Mobile fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style Admin fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style AUtils fill:#e94560,stroke:#fff,color:#fff
style AUI fill:#ff9800,stroke:#fff,color:#fff
style AWeb fill:#ff9800,stroke:#fff,color:#fff
style AMobile fill:#ff9800,stroke:#fff,color:#fff
style AAdmin fill:#ff9800,stroke:#fff,color:#fff
Sửa Utils → Nx tự detect UI, Web, Mobile, Admin đều bị affected và cần rebuild.
Module Boundaries — Ngăn chặn Architectural Drift
Một trong những tính năng mạnh nhất của Nx là enforce module boundaries. Team định nghĩa rule: "app chỉ được import từ lib, lib không được import từ app, lib type:feature không được import lib type:feature khác".
// project.json hoặc package.json
{
"tags": ["type:feature", "scope:auth"]
}
// .eslintrc.json — root
{
"rules": {
"@nx/enforce-module-boundaries": ["error", {
"depConstraints": [
{ "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"] },
{ "sourceTag": "type:feature", "onlyDependOnLibsWithTags": ["type:ui", "type:util"] },
{ "sourceTag": "type:ui", "onlyDependOnLibsWithTags": ["type:util"] }
]
}]
}
}
Nếu developer vô tình import feature lib từ UI lib → ESLint báo lỗi ngay lập tức. Không cần code review để phát hiện — tooling làm thay.
Code Generators — Tạo project từ template
Nx generators tự động tạo project mới với đầy đủ config, tuân thủ chuẩn team:
# Tạo Vue library mới
nx generate @nx/vue:library shared-components --directory=packages
# Tạo Node.js API mới
nx generate @nx/node:application payment-service --directory=apps
# Custom generator của team
nx generate @myorg/workspace-plugin:feature-lib auth-module
Generators thực hiện AST-level TypeScript transformation, tự động update tsconfig paths, workspace references và CI config.
Distributed Task Execution — Nx Agents
Với monorepo lớn, build trên 1 máy vẫn chậm dù có caching. Nx Agents phân tán workload across máy, sử dụng historical timing data để cân bằng:
| Setup | Turborepo | Nx |
|---|---|---|
| Single machine CI | 25 phút 32 giây | 21 phút 56 giây |
| Distributed (4 machines) | Không hỗ trợ native | 9 phút 20 giây |
| Cải thiện so với single | — | 57% nhanh hơn |
Khi nào Nx là lựa chọn đúng?
Team 10+ người, 30+ packages, cần architectural enforcement, code generation và CI phân tán. Nx phù hợp đặc biệt với polyglot repo (TypeScript + .NET + Python) nhờ plugin ecosystem mở rộng.
Bazel — Vũ khí của Tech Giants
Bazel (by Google) hoạt động ở quy mô khác hẳn: 100.000+ source files, hàng trăm developer, nhiều ngôn ngữ. Nó cung cấp hermetic builds (hoàn toàn tách biệt khỏi môi trường local), fine-grained caching ở mức action (nhỏ hơn task), và remote execution phân tán build trên cluster máy.
Case study thực tế
- Stripe: 300+ services trong một monorepo, CI giảm từ 45 phút xuống dưới 7 phút
- JPMorgan Chase: 850+ microservices, build time giảm 40%
- Google: 100.000 source files, xác định cần recompile trong ~200ms
Tuy nhiên, Bazel có learning curve rất dốc: cần học Starlark (ngôn ngữ config riêng), viết BUILD files chi tiết cho mọi dependency. Chỉ phù hợp với team 100+ engineer hoặc codebase polyglot phức tạp.
So sánh tổng thể: Turborepo vs Nx vs Bazel
| Tiêu chí | Turborepo | Nx | Bazel |
|---|---|---|---|
| Setup time | < 30 phút | 1-2 giờ | 1-2 tuần |
| Learning curve | Thấp | Trung bình | Rất cao |
| Task caching | Content-aware hashing | Named inputs, ing | Action-level, hermetic |
| Remote caching | Vercel hoặc self-hosted | Nx Cloud hoặc self-hosted | Remote Build Execution |
| Affected detection | Filter syntax | Graph-based, file-level | Fine-grained, ~200ms |
| Distributed CI | Không native | Nx Agents | Remote Execution |
| Code generators | Không | AST-level generators | Không (BUILD files) |
| Module boundaries | Không native | ESLint rule enforcement | Visibility rules |
| Polyglot | JS/TS focused | JS/TS + .NET, Java, Go, Python | Mọi ngôn ngữ |
| IDE integration | Không | Nx Console (VS Code, JetBrains) | Bazel plugin |
| Phù hợp team | 3-20 người | 10-100 người | 100+ người |
CI/CD Optimization cho Monorepo
CI/CD là nơi monorepo bộc lộ rõ nhất cả ưu và nhược điểm. Không optimize → mỗi PR trigger build toàn bộ repo → CI chậm hơn polyrepo. Optimize đúng cách → chỉ build affected packages → CI nhanh gấp nhiều lần.
Chiến lược Affected-Only Pipeline
graph LR
A[PR Created] --> B[Detect Changed Files]
B --> C[Resolve Dependency Graph]
C --> D[Identify Affected Packages]
D --> E{Cache Hit?}
E -->|Yes| F[Skip Build]
E -->|No| G[Build + Test]
G --> H[Upload to Remote Cache]
F --> I[✅ CI Pass]
H --> I
style A fill:#2c3e50,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style C fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style D fill:#e94560,stroke:#fff,color:#fff
style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style F fill:#4CAF50,stroke:#fff,color:#fff
style G fill:#2c3e50,stroke:#fff,color:#fff
style H fill:#2c3e50,stroke:#fff,color:#fff
style I fill:#4CAF50,stroke:#fff,color:#fff
CI pipeline tối ưu: chỉ build affected packages, skip khi cache hit.
GitHub Actions với Turborepo
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Cần commit trước để so sánh
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm turbo build test lint --filter=...[HEAD~1]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
--filter=...[HEAD~1] chỉ chạy tasks cho packages thay đổi kể từ commit trước. Kết hợp remote cache, phần lớn tasks sẽ cache hit → CI finish trong 1-2 phút thay vì 15-20 phút.
GitHub Actions với Nx
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history cho affected detection
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4
- run: pnpm nx affected -t lint test build --parallel=3
Code Ownership và Governance
Monorepo gộp code nhưng không gộp trách nhiệm. Thiếu ownership rõ ràng → changes không ai review → chất lượng giảm dần. Đây là lý do CODEOWNERS file trở thành bắt buộc trong monorepo.
# CODEOWNERS
/apps/web/ @team-frontend
/apps/api/ @team-backend
/packages/ui/ @team-design-system
/packages/auth/ @team-security
/packages/config/ @team-platform
*.yml @team-devops
Anti-pattern: Shared package không có owner
Khi một shared package (ví dụ packages/utils) không có team chịu trách nhiệm, ai cũng có thể thêm function vào đó mà không ai review cẩn thận. Dần dần nó trở thành "junk drawer" chứa đủ thứ không liên quan. Luôn assign owner cho mọi package, kể cả shared.
Best Practices cho Production Monorepo
1. Bắt đầu từ pnpm Workspaces + Turborepo
Đừng nhảy thẳng vào Nx hay Bazel. Bắt đầu với combo đơn giản nhất: pnpm quản lý dependencies, Turborepo chạy tasks. Khi team hoặc codebase lớn lên đến mức cần module boundaries hay distributed CI, migrate sang Nx.
2. Tách rõ apps/ và packages/
apps/ chứa deployable applications (web, api, mobile). packages/ chứa reusable libraries (ui, utils, config). App import package, package không import app. Quy tắc đơn giản nhưng ngăn chặn circular dependencies hiệu quả.
3. Shared config ở root
TypeScript config, ESLint config, Prettier config — đặt ở root và extends từ package con. Tránh copy-paste config giữa các packages.
// packages/web/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
}
}
4. Versioning strategy: Fixed vs Independent
| Strategy | Mô tả | Phù hợp khi |
|---|---|---|
| Fixed (locked) | Tất cả packages cùng version | Internal packages, không publish ra npm |
| Independent | Mỗi package version riêng | Publish ra npm, nhiều consumer khác nhau |
Nx hỗ trợ cả hai qua nx release. Turborepo kết hợp với changesets cho versioning.
5. Git sparse checkout cho developer experience
Khi repo lớn đến mức clone mất nhiều phút, dùng sparse checkout để chỉ tải phần code cần thiết:
git clone --filter=blob:none --sparse https://github.com/myorg/monorepo.git
cd monorepo
git sparse-checkout set apps/web packages/ui packages/utils
6. Enforced conventional commits
Với nhiều team commit vào cùng repo, conventional commits (feat(auth):, fix(ui):, chore(ci):) giúp changelog tự động và dễ filter history theo scope.
Migration Path: Từ Polyrepo sang Monorepo
Kết luận
Monorepo 2026 không còn là lựa chọn "đại gia công nghệ mới dùng được". Với hệ sinh thái tooling đã trưởng thành — pnpm Workspaces cho dependency management, Turborepo cho task caching, Nx cho workspace governance — team từ 5 người trở lên đều có thể hưởng lợi.
Bắt đầu đơn giản với pnpm + Turborepo. Khi nào thấy cần kiểm soát architectural boundaries hoặc CI chậm trên single machine → nâng lên Nx. Bazel chỉ cần khi repo vượt quy mô hàng trăm nghìn files đa ngôn ngữ.
Điều quan trọng nhất không phải tool mà là culture: code ownership rõ ràng, conventional commits, shared config, và CI pipeline đo lường liên tục. Monorepo chỉ là vehicle — team discipline mới là engine.
Tóm tắt lựa chọn
- Team nhỏ (3-20), JS/TS thuần: pnpm Workspaces + Turborepo
- Team trung bình (10-100), cần governance: pnpm + Nx
- Enterprise (100+), đa ngôn ngữ: Bazel hoặc Nx + plugins
Nguồn tham khảo:
OpenTelemetry — Tiêu chuẩn Observability cho hệ thống phân tán
Saga Pattern: Quản lý Distributed Transactions trong Microservices
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.