Monorepo vs Polyrepo: dependency graphs, build orchestration, CI strategy
Monorepo or Polyrepo — the eternal backend architecture debate. This is the real cost comparison of both layouts, and how to use PlantUML to draw service / package dependency graphs, Bazel/Nx/Turborepo build orchestration, and CI incremental build strategy.
Three repo layouts
Polyrepo (one repo per service)
1 | git@github.com:org/user-service.git |
Monorepo (all services in one repo)
1 | git@github.com:org/platform.git |
Multi-repo (middle ground, several related services per repo)
1 | git@github.com:org/checkout-platform.git |
How to choose
| Dimension | Polyrepo | Monorepo |
|---|---|---|
| Code visibility | poor — cross-repo view is painful | good — IDE full-text search |
| Cross-service changes | hard — multiple PRs to coordinate | easy — single PR |
| Build complexity | simple — each builds itself | complex — dependency orchestration |
| CI speed | simple — build only self | complex — incremental build |
| Team autonomy | high — each team’s rhythm | low — synchronized rhythm |
| Permission management | fine-grained — per-repo ACL | coarse — CODEOWNERS |
| Git performance | good — small repo | poor — git blame slow on big repo |
| Suitable scale | <10 services, >200 services | 10-100 services |
Rule of thumb:
- <10 services → Polyrepo, simple
- 10-50 services, tightly coupled → Monorepo (Google/FB/Meta pattern)
- >100 services → Multi-repo (aggregate by business domain)
Monorepo dependency graph
1 | @startuml |
Drawing dependency graphs tips:
- solid line = strong dep (compile-time / direct import)
- dotted
..>= weak dep (config / network / indirect) - package grouping = by business domain
- Platform components at the bottom — depended on by many services
Detecting circular dependencies
1 | # check_cycles.py |
CI runs this — any cycle fails the build.
Build tool: Bazel
Bazel is Google’s internal Blaze open-sourced — uses BUILD files to describe dependencies:
1 | # services/user/BUILD |
Dependency graph:
1 | @startuml |
Bazel’s incremental build:
1 | # change one file, only rebuild affected targets |
Build tool: Nx (Nx.dev)
Nx is a monorepo build tool, suits JavaScript/TypeScript:
1 | // nx.json |
1 | # incremental build (only build affected) |
Nx dependency graph auto-generated:
1 | nx dep-graph |
Build tool: Turborepo
Turborepo is from Vercel, suited for Next.js / Vite projects:
1 | // turbo.json |
1 | # incremental build |
CI strategy
Polyrepo CI
1 | # .github/workflows/build.yml |
Simple — one repo, one CI.
Monorepo CI (full build)
1 | name: Build All |
Slow — 50 services × 5 min build = 4 hours.
Monorepo CI (incremental)
1 | name: Build Affected |
Key: fetch-depth: 0 — needs git history to compute affected. Nx’s affected algorithm: diff files in base..head, reverse-trace the dependency graph, build all services on the dependency chain.
Example: changed libs/common-utils, 10 services depend on it → all need rebuild → 10 × 5 min = 50 min.
Monorepo CI (incremental + remote cache)
1 | - run: nx affected -t build --base=origin/main --head=HEAD |
Remote cache: CI’s builds cache to Nx Cloud, local nx build reuses them. Same code change → no rebuild.
Polyrepo → Monorepo migration
Field steps:
1 | @startuml |
Key: gradual migration, not big-bang. Verify production after each service migration.
Field foot-guns
- Monorepo without directory structure — all services flat, tens of thousands of files mess. Strictly organize by
services/libs/tools/. - No dependency graph visualization — newcomers don’t know which service depends on which. Nx’s
nx dep-graphoutputs SVG, put it on the wiki. - Shared lib change breaks everything — change one line in libs/common-utils, all services break. All lib changes must go through CI + all services’ tests.
- CI doesn’t build incrementally — full build runs hours. Must use Nx affected / Turborepo –filter / Bazel remote cache.
- Permission chaos — all services see all code, audit is hard. Use CODEOWNERS:
1
2
3/services/payment/ @payments-team
/libs/auth-client/ @security-team
/libs/db-helper/ @platform-team - Git performance poor —
git logslow on big repo. Usegit log -- <path>for single file, ortigto speed up. - IDE lag — VSCode first-time indexing slow on monorepo. Exclude unwanted directories:
"files.exclude": {"**/node_modules": true, "**/dist": true}.
Decision tree
1 | How big is the team? |
Minimum viable monorepo: Nx + npm workspaces + nx affected + GitHub Actions running incremental. Big team / perf-critical upgrade to Bazel.
Minimum viable polyrepo + shared libs: separate platform-shared-lib repo, other services reference via git submodule / version tags / private npm registry.
Remember: Monorepo is a tool, not a religion. Google/Facebook use Monorepo because they have dedicated monorepo teams maintaining the build system. Small teams forcing monorepo actually hurt productivity.
- Title: Monorepo vs Polyrepo: dependency graphs, build orchestration, CI strategy
- Author: puml.online
- Created at : 2026-07-30 17:40:00
- Updated at : 2026-08-14 21:34:29
- Link: https://puml.online/blog/plantuml-monorepo-polyrepo-en/
- License: This work is licensed under CC BY-NC-SA 4.0.