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

36M+ Nx NPM downloads/tháng (2026)
90% Giảm CI time với Remote Caching
70-85% Build nhanh hơn so với polyrepo
850+ Microservices trong monorepo JPMorgan

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íMonorepoPolyrepo
Dependency syncMột lockfile, version luôn đồng nhấtMỗi repo lockfile riêng, dễ drift
Code sharingImport trực tiếp, không cần publishPhải publish lên registry rồi install
Atomic changes1 PR thay đổi cả API + frontendPhải sync nhiều PR across repos
CI/CD phức tạpCần affected detection, cachingĐơn giản: mỗi repo 1 pipeline
Git performanceCần sparse checkout khi repo lớnMỗi repo nhẹ nhàng
Team autonomyModule boundaries + CODEOWNERSTự 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:

SetupTurborepoNx
Single machine CI25 phút 32 giây21 phút 56 giây
Distributed (4 machines)Không hỗ trợ native9 phút 20 giây
Cải thiện so với single57% 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íTurborepoNxBazel
Setup time< 30 phút1-2 giờ1-2 tuần
Learning curveThấpTrung bìnhRất cao
Task cachingContent-aware hashingNamed inputs, ingAction-level, hermetic
Remote cachingVercel hoặc self-hostedNx Cloud hoặc self-hostedRemote Build Execution
Affected detectionFilter syntaxGraph-based, file-levelFine-grained, ~200ms
Distributed CIKhông nativeNx AgentsRemote Execution
Code generatorsKhôngAST-level generatorsKhông (BUILD files)
Module boundariesKhông nativeESLint rule enforcementVisibility rules
PolyglotJS/TS focusedJS/TS + .NET, Java, Go, PythonMọi ngôn ngữ
IDE integrationKhôngNx Console (VS Code, JetBrains)Bazel plugin
Phù hợp team3-20 người10-100 người100+ 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

StrategyMô tảPhù hợp khi
Fixed (locked)Tất cả packages cùng versionInternal packages, không publish ra npm
IndependentMỗi package version riêngPublish 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

Giai đoạn 1: Pilot (1-2 tuần)
Chọn 2-3 repos liên quan chặt (ví dụ web app + shared UI). Gộp vào monorepo với pnpm workspaces. Setup Turborepo cơ bản. Chạy song song với polyrepo cũ.
Giai đoạn 2: CI/CD (1 tuần)
Setup affected-only pipeline trên GitHub Actions. Enable remote caching. Benchmark CI time so với polyrepo cũ — target giảm ≥50%.
Giai đoạn 3: Team onboarding (1-2 tuần)
Migrate thêm repos. Setup CODEOWNERS. Document workspace conventions. Training cho team về turbo/nx commands.
Giai đoạn 4: Scale (ongoing)
Evaluate Nx nếu cần module boundaries hoặc distributed CI. Implement code generators cho project scaffolding chuẩn hóa. Monitor CI metrics liên tục.

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: