cover

前言:版本庫策略決定了一切

流程概覽

flowchart TD
    Q{版本庫策略<br/>選擇}
    Q -->|緊密耦合<br/>共用程式碼多<br/>團隊 < 15 人| MONO[Monorepo]
    Q -->|獨立部署<br/>技術棧多元<br/>團隊 > 50 人| MULTI[Multirepo]
    Q -->|混合需求| HYBRID[混合策略]

    MONO --> M1[單一倉庫]
    MONO --> M2[共享工具鏈]
    MONO --> M3[原子性變更]

    MULTI --> U1[獨立倉庫]
    MULTI --> U2[獨立 CI/CD]
    MULTI --> U3[團隊自治]

    HYBRID --> H1[相關服務 Monorepo]
    HYBRID --> H2[獨立系統 Multirepo]

    style Q fill:#FF9800,color:#fff
    style MONO fill:#2196F3,color:#fff
    style MULTI fill:#4CAF50,color:#fff
    style HYBRID fill:#9C27B0,color:#fff

版本庫(Repository)的組織方式,看似只是「程式碼放哪裡」的問題,實際上卻深刻影響著團隊的每一個工作面向:

  • CI/CD 速度:一個擁有百萬行程式碼的 Monorepo,每次 push 都要跑完整個 pipeline?還是每個小 repo 只跑自己的 30 秒 build?
  • 程式碼共享:跨專案共用的 utility、UI 元件、型別定義,要怎麼管理版本與相依性?
  • 團隊協作:20 個工程師同時在同一個 repo 裡面改 code,merge conflict 滿天飛?還是各自在獨立的 repo 中自由開發?
  • 部署獨立性:改了 shared library 的一行程式碼,是要全部服務重新部署,還是只有受影響的服務需要更新?

這些問題沒有標準答案。但理解 Monorepo 和 Multirepo 各自的優勢與代價,能幫助你在專案初期就做出更好的架構決策,避免日後痛苦的遷移。


架構全景:Monorepo vs Multirepo

以下用一張圖來呈現兩種策略的核心差異:

graph TB
    subgraph Monorepo["Monorepo(單一版本庫)"]
        direction TB
        MR["my-company-repo/"]
        MR --> PA["packages/ui-library"]
        MR --> PB["packages/utils"]
        MR --> PC["packages/api-client"]
        MR --> AA["apps/web-app"]
        MR --> AB["apps/mobile-app"]
        MR --> AC["apps/admin-panel"]
        MR --> SA["shared/config"]
        MR --> SB["shared/types"]
    end

    subgraph Multirepo["Multirepo(多版本庫)"]
        direction TB
        R1["repo: ui-library"]
        R2["repo: utils"]
        R3["repo: api-client"]
        R4["repo: web-app"]
        R5["repo: mobile-app"]
        R6["repo: admin-panel"]

        R4 -.->|"npm install"| R1
        R4 -.->|"npm install"| R2
        R5 -.->|"npm install"| R3
        R6 -.->|"npm install"| R1
    end

    style Monorepo fill:#E3F2FD,stroke:#1565C0
    style Multirepo fill:#FFF3E0,stroke:#E65100

Monorepo 把所有專案放在同一個 Git 倉庫中,透過目錄結構來區隔;Multirepo 則讓每個專案擁有自己獨立的 Git 倉庫,透過套件管理器(npm/yarn/pnpm)來管理跨專案的相依關係。


核心概念

Monorepo:一個倉庫,多個專案

Monorepo(Monolithic Repository)是指將多個專案、套件或服務放在同一個版本控制倉庫中的策略。業界最知名的採用者包括:

  • Google:整個公司超過 20 億行程式碼放在單一倉庫中,使用自研的 Piper 系統管理
  • Meta(Facebook):使用自研的 Buck 建構系統搭配 Mercurial
  • Microsoft:Windows 程式碼庫使用 GVFS(Git Virtual File System)來管理超大型 Monorepo
  • Uber、Twitter、Airbnb:也都是 Monorepo 的擁護者

Monorepo 的目錄結構通常如下:

my-monorepo/
├── apps/                          # 應用程式
│   ├── web/                       # 前端 Web 應用
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── api/                       # 後端 API 服務
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── admin/                     # 管理後台
│       ├── src/
│       ├── package.json
│       └── tsconfig.json
├── packages/                      # 共用套件
│   ├── ui/                        # UI 元件庫
│   │   ├── src/
│   │   │   ├── Button/
│   │   │   ├── Modal/
│   │   │   └── index.ts
│   │   └── package.json
│   ├── utils/                     # 工具函式
│   │   ├── src/
│   │   │   ├── date.ts
│   │   │   ├── format.ts
│   │   │   └── index.ts
│   │   └── package.json
│   └── api-client/                # API 客戶端 SDK
│       ├── src/
│       └── package.json
├── shared/                        # 全域共用設定
│   ├── eslint-config/             # 統一 ESLint 規則
│   ├── tsconfig/                  # 統一 TypeScript 設定
│   └── jest-config/               # 統一測試設定
├── package.json                   # Root package.json
├── pnpm-workspace.yaml            # Workspace 設定
├── turbo.json                     # Turborepo 設定
└── nx.json                        # 或 Nx 設定

Multirepo(Polyrepo):每個專案獨立一個倉庫

Multirepo 策略讓每個專案或服務擁有自己獨立的 Git 倉庫。共用程式碼以 npm package 的形式發佈到 registry(npm、GitHub Packages 或私有 registry),其他專案透過 npm install 來引用。

這也是大多數開源專案和中小型團隊最自然的選擇。


Monorepo 解決了什麼問題?

1. 原子性變更(Atomic Changes)

想像一個情境:你的 api-client 套件新增了一個欄位,前端的 web-appadmin 都需要配合修改。

  • Multirepo 的做法:先在 api-client repo 改 code → 發版 v1.2.3 → 到 web-app repo 更新相依版本 → 改前端 code → 再到 admin repo 做同樣的事。三個 PR、三次 code review、三次部署,中間任何一步漏掉都會造成版本不一致。
  • Monorepo 的做法:一個 PR 同時改 api-clientweb-appadmin,一次 review、一次合併,保證所有變更是原子性的。

2. 統一的 CI/CD Pipeline

所有專案共用同一套 CI 設定,不需要在每個 repo 都維護一份 .gitlab-ci.ymlGitHub Actions。工具鏈的升級(ESLint 規則、TypeScript 版本、測試框架)也只需要改一次。

3. 共用工具鏈與設定

ESLint、Prettier、TypeScript、Jest 的設定只需要維護一份。新加入團隊的成員 clone 一個 repo 就能開始開發所有專案,不需要搞清楚十幾個 repo 各自的開發環境設定。

4. 消除版本不一致(Dependency Hell)

在 Multirepo 中,web-app@company/utils@1.2.0admin@company/utils@1.3.0,這種版本飄移(version drift)非常常見。在 Monorepo 中,所有專案永遠使用同一版本的共用套件。


Monorepo 創造了什麼問題?

1. CI/CD 速度急劇下降

當倉庫裡有 50 個套件和 10 個應用,每次 push 都要跑完整的 lint、test、build 嗎?即使只改了一行 README?

雖然 Nx 和 Turborepo 透過「affected analysis」和「task caching」大幅改善了這個問題,但這些工具本身也帶來了額外的學習成本與維護成本。

2. 複雜的工具鏈

要讓 Monorepo 順暢運作,你需要:

  • Workspace 管理(pnpm workspace / yarn workspace / npm workspace)
  • 建構排程(Nx / Turborepo / Lerna)
  • 變更偵測(only build what changed)
  • 遠端快取(Nx Cloud / Turborepo Remote Cache)
  • 可能還需要自訂的 Git hooks 和 CI 腳本

對於一個三人小團隊來說,這些工具鏈的投資可能遠超過它帶來的收益。

3. 爆炸半徑(Blast Radius)

在 Monorepo 中,一個糟糕的 commit 可能影響所有專案。一個 breaking change 被合併進 shared/utils,所有依賴它的 10 個應用都會同時壞掉。

4. 耦合的誘惑

Monorepo 讓跨專案引用變得太容易了。工程師可能會直接 import 另一個應用的內部模組,而不是透過定義清楚的 public API。久而久之,專案之間的邊界模糊,耦合越來越深,最終變成一個巨大的泥球(Big Ball of Mud)。

5. Git 效能問題

當倉庫變得非常大(數十萬個檔案、數十萬個 commit),git clonegit statusgit log 都會變慢。雖然 git sparse-checkoutgit shallow clone 可以緩解,但這又是額外的複雜度。


為什麼有些團隊正在回歸小專案控管?

近年來,一個有趣的趨勢是:一些原本擁抱 Monorepo 的團隊開始將部分服務拆回獨立的 repo。這背後的原因包括:

微服務的獨立性

微服務架構的核心精神是「各自獨立部署、獨立演進」。當一個微服務被鎖在 Monorepo 裡面,它的 CI/CD pipeline、部署節奏、技術選型都被其他服務牽制。拆出去之後,團隊可以自由選擇框架、語言甚至建構工具。

更簡單的 CI/CD

獨立 repo 的 CI 設定就是簡簡單單的「lint → test → build → deploy」,不需要 affected analysis、task graph、remote cache 這些額外層級。CI 壞了很容易排查,因為範圍就在這個 repo 裡面。

團隊自治(Team Autonomy)

在大型組織中,不同團隊負責不同的服務。如果所有服務都在同一個 Monorepo,團隊的 code review 規則、分支策略、release 節奏都被迫統一。拆分成獨立 repo 後,每個團隊可以按照自己的節奏和方式工作。

權限控管更直覺

在 Monorepo 中,要實現「團隊 A 只能改 apps/web 目錄」這種權限控管需要額外的 CODEOWNERS 設定和 CI 檢查。在 Multirepo 中,repo 層級的權限就是最自然的隔離邊界。


決策框架:何時該用哪種策略?

選擇 Monorepo 的時機

條件說明
緊密耦合的服務前端 + BFF + 共用型別,經常需要一起改
共用基礎設施多UI 元件庫、工具函式、設定檔等共用程式碼佔比高
團隊規模小少於 15-20 人,溝通成本低
技術棧統一全部都是 TypeScript / Python / Go,不需要多種建構系統
發版節奏一致所有服務通常一起部署

選擇 Multirepo 的時機

條件說明
微服務邊界清晰各服務之間透過 API 溝通,幾乎不共用程式碼
組織規模大超過 5 個團隊、50 人以上,需要團隊自治
部署節奏不同有些服務一天部署 10 次,有些一個月才部署一次
技術棧多元前端用 TypeScript、後端用 Go、ML 用 Python
開源或跨組織協作需要公開的 repo 讓外部貢獻者參與

混合策略(Hybrid Approach)

實務上,很多成熟的團隊會採用混合策略:

  • Monorepo A:前端應用群(web、admin、mobile)+ 共用 UI 元件 + 共用工具函式
  • Monorepo B:後端微服務群(user-service、order-service、payment-service)+ 共用 proto 定義
  • 獨立 Repo C:ML Pipeline(完全不同的技術棧和部署流程)
  • 獨立 Repo D:Infrastructure as Code(Terraform/Pulumi,由 SRE 團隊獨立管理)

這種策略保留了相關服務之間的 Monorepo 優勢,同時讓本質上獨立的系統擁有完整的自主權。


Monorepo 工具鏈比較

Nx

Nx 是目前功能最完整的 Monorepo 工具,由 Nrwl 團隊開發(多位成員來自 Angular 團隊)。

核心特色:

  • 智慧快取(Local & Remote Caching)
  • 相依圖分析(Dependency Graph Visualization)
  • 程式碼產生器(Code Generators)
  • 支援多種框架(React、Angular、Vue、Node.js、Go)
  • 受影響分析(Affected Commands)

Nx Workspace 設定範例:

// nx.json
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "namedInputs": {
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"],
    "production": [
      "default",
      "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
      "!{projectRoot}/tsconfig.spec.json",
      "!{projectRoot}/jest.config.[jt]s"
    ]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
      "cache": true
    },
    "lint": {
      "inputs": [
        "default",
        "{workspaceRoot}/.eslintrc.json",
        "{workspaceRoot}/.eslintignore"
      ],
      "cache": true
    }
  },
  "nxCloudAccessToken": "your-nx-cloud-token",
  "defaultBase": "main"
}

使用 Nx 時,只建構受影響的專案:

# 只測試受 main 分支以來變更影響的專案
npx nx affected -t test --base=main
 
# 視覺化相依圖
npx nx graph
 
# 只建構特定專案及其相依
npx nx build web-app

Turborepo

Turborepo 由 Vercel 收購並維護,主打「簡單、快速」。相比 Nx 的全功能框架,Turborepo 更像是一個輕量級的建構排程器。

Turborepo Pipeline 設定範例:

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "globalEnv": ["NODE_ENV", "API_URL"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json", "package.json"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["NODE_ENV", "API_URL"]
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**", "test/**", "**/*.test.*"],
      "outputs": ["coverage/**"],
      "env": ["CI"]
    },
    "lint": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", ".eslintrc.*", "tsconfig.json"],
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "type-check": {
      "dependsOn": ["^build"],
      "inputs": ["src/**", "tsconfig.json"],
      "outputs": []
    }
  }
}

Lerna

Lerna 是最早期的 JavaScript Monorepo 工具之一,現在由 Nx 團隊接手維護。它的核心功能是管理多套件的版本發佈。

適用場景:如果你的 Monorepo 主要是一組 npm 套件,需要統一管理版本號和發佈流程,Lerna 仍然是不錯的選擇。

Bazel

Bazel 是 Google 開源的建構系統,是 Google 內部 Blaze 的外部版本。

優點:極致的建構快取、跨語言支援(Java、C++、Go、Python、JS)、遠端執行。 缺點:學習曲線極陡、設定複雜、生態系較小。

適用場景:超大型組織、多語言 Monorepo、需要極致的建構效能。

工具比較總覽

特性NxTurborepoLernaBazel
學習曲線中等極高
快取Local + RemoteLocal + Remote透過 NxHermetic Cache
相依圖視覺化基礎強大
程式碼產生內建
多語言有限JS/TS 為主JS/TS完整支援
社群生態豐富成長中成熟較小
適合規模中大型小中型小型超大型

GitLab CI/CD 搭配 Monorepo

在 Monorepo 中,CI 最重要的優化是「只建構受變更影響的套件」。以下是一個 GitLab CI 的範例:

# .gitlab-ci.yml — Monorepo CI with change detection
stages:
  - detect
  - lint
  - test
  - build
  - deploy
 
variables:
  NX_BASE: $CI_MERGE_REQUEST_DIFF_BASE_SHA
  NX_HEAD: $CI_COMMIT_SHA
 
# 偵測哪些專案受到影響
detect-changes:
  stage: detect
  image: node:20-alpine
  script:
    - npx nx show projects --affected --base=$NX_BASE --head=$NX_HEAD > affected.txt
    - echo "Affected projects:"
    - cat affected.txt
  artifacts:
    paths:
      - affected.txt
 
# 只 lint 受影響的專案
lint:affected:
  stage: lint
  image: node:20-alpine
  needs: ["detect-changes"]
  script:
    - pnpm install --frozen-lockfile
    - npx nx affected -t lint --base=$NX_BASE --head=$NX_HEAD --parallel=3
  rules:
    - if: $CI_MERGE_REQUEST_ID
    - if: $CI_COMMIT_BRANCH == "main"
 
# 只測試受影響的專案
test:affected:
  stage: test
  image: node:20-alpine
  needs: ["detect-changes"]
  script:
    - pnpm install --frozen-lockfile
    - npx nx affected -t test --base=$NX_BASE --head=$NX_HEAD --parallel=3 --ci
  coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
  artifacts:
    reports:
      junit: "**/junit.xml"
      coverage_report:
        coverage_format: cobertura
        path: "**/coverage/cobertura-coverage.xml"
 
# 只建構受影響的專案
build:affected:
  stage: build
  image: node:20-alpine
  needs: ["lint:affected", "test:affected"]
  script:
    - pnpm install --frozen-lockfile
    - npx nx affected -t build --base=$NX_BASE --head=$NX_HEAD --parallel=3
  artifacts:
    paths:
      - apps/*/dist/
      - packages/*/dist/
 
# 部署特定應用(只在 main 分支且該應用受影響時觸發)
deploy:web-app:
  stage: deploy
  needs: ["build:affected"]
  script:
    - |
      if grep -q "web-app" affected.txt; then
        echo "Deploying web-app..."
        # 你的部署指令
      else
        echo "web-app not affected, skipping deploy."
      fi
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

演進敘事:從單一 Repo 到 Monorepo,再回到小 Repo

版本庫策略的演進並不是一條直線,而是一個鐘擺。理解這個鐘擺的擺動規律,能幫助你在不同階段做出正確的判斷。

階段一:什麼都放在一起(2000s - 2010s)

早期的軟體專案通常就是一個 repo 一個應用。隨著專案變大,前端、後端、shared library 自然而然地放在同一個 repo 裡面。這本質上就是一個未經管理的 Monorepo。

問題:沒有模組邊界,everything depends on everything。部署就是整包丟上去。

階段二:拆分成微服務 + Multirepo(2010s - 2015s)

微服務架構興起後,團隊開始將大型應用拆分成獨立的服務,每個服務一個 repo。這帶來了部署獨立性和團隊自治。

問題:跨服務的變更變得痛苦,版本飄移嚴重,每個 repo 的工具鏈和設定各自為政。

階段三:擁抱 Monorepo(2015s - 2020s)

看到 Google、Meta 等巨頭的成功案例,加上 Lerna、Nx、Turborepo 等工具的成熟,許多團隊開始將專案合併到 Monorepo 中。

問題:CI 變慢、工具鏈變複雜、Git 效能下降、團隊自治被限制。

階段四:務實的混合策略(2020s - 現在)

經歷過前幾個階段的洗禮後,越來越多團隊開始採用務實的混合策略:

  • 相關性高的專案放在同一個 Monorepo
  • 獨立性高的服務放在自己的 repo
  • 共用套件視情況決定:如果只有 2-3 個消費者,放在 Monorepo;如果有 10+ 個消費者,獨立發佈到 npm registry

這個鐘擺告訴我們:沒有銀彈,只有 trade-off。


常見問題與風險

1. Monorepo 的 Git Clone 太慢怎麼辦?

使用 git sparse-checkout 只下載你需要的目錄,搭配 --filter=blob:none 實現 partial clone:

git clone --filter=blob:none --sparse https://github.com/my-org/monorepo.git
cd monorepo
git sparse-checkout set apps/web-app packages/ui packages/utils

2. Multirepo 的版本一致性怎麼維持?

使用 Renovate 或 Dependabot 自動化相依版本升級,搭配語意化版本(Semantic Versioning)和 Changelog 自動產生。

3. Monorepo 的權限控管怎麼做?

使用 GitHub CODEOWNERS 或 GitLab Code Owners 功能,搭配 CI 中的路徑觸發規則(path-based triggers),確保只有負責的團隊能核准特定目錄的變更。

4. 從 Multirepo 遷移到 Monorepo 的步驟?

  1. 選擇 Monorepo 工具(推薦 Nx 或 Turborepo)
  2. 建立 Monorepo 骨架(root package.json、workspace 設定)
  3. 逐步將 repo 搬入,保留 Git 歷史(使用 git subtree 或手動搬移)
  4. 統一工具鏈設定(ESLint、TypeScript、Jest)
  5. 設定 CI/CD 的受影響偵測
  6. 將 npm package 的跨 repo 依賴改為 workspace 內部依賴

5. Monorepo 中不同專案要用不同的 Node.js 版本怎麼辦?

使用 Docker 化的 CI pipeline,或透過 voltafnm 等工具管理每個專案的 Node.js 版本。不過這確實增加了複雜度,如果版本差異很大,可能是該拆分成獨立 repo 的信號。


決策檢查清單

在做出最終決定之前,問自己以下問題:

  • 你的團隊有多少人?超過 30 人可能需要考慮 Multirepo 的團隊自治優勢
  • 你的服務之間有多少共用程式碼?共用越多,Monorepo 優勢越大
  • 你的部署節奏是否一致?不一致的話,Monorepo 可能造成不必要的牽制
  • 你有足夠的基礎設施能力維護 Monorepo 工具鏈嗎?
  • 你的 CI pipeline 能在 10 分鐘內完成嗎?超過的話需要投資快取與受影響偵測
  • 團隊成員是否熟悉 Nx / Turborepo?學習成本是否可接受?

總結

面向MonorepoMultirepo
原子性變更天然支援需要跨 repo 協調
CI/CD 複雜度高(需要受影響偵測)低(各自獨立)
程式碼共享簡單直接需要發佈套件
團隊自治較受限天然支援
部署獨立性需要額外設計天然支援
工具鏈投資
新人上手Clone 一次全部搞定需要 clone 多個 repo
Git 效能隨規模下降始終穩定

最終建議:不要因為 Google 用 Monorepo 就跟著用,也不要因為 Monorepo 聽起來很酷就跟著用。評估你的團隊規模、服務耦合度、技術棧統一程度和 CI 基礎設施,選擇最務實的策略。如果不確定,從 Multirepo 開始通常是更安全的選擇——把東西合在一起永遠比拆開來容易。


Proto 實踐對照

目前 Proto 採用 Multi-repo 策略:Django / Vue / Flutter 各自獨立 repo,Infra 層也是獨立 repo。Proto 透過 Git Submodule 被 Infra 拉入部署流程。這個策略適合目前的規模,但如果未來 Proto 數量增多(多語言 + 多 Track),可以考慮遷移到 Monorepo。詳見 Proto 規劃方法論 的多語言延伸策略。


延伸閱讀