Monorepo vs Polyrepo:
The Same Task, Side-by-Side
Five common engineering tasks shown side-by-side in monorepo vs polyrepo. Real command sequences, 2026 tooling references (Nx, Turborepo, GitHub Actions). Each task ends with a verdict.
1. Bump a shared library version
Mono: one PR, one diff, atomic. Poly: publish bump then per-consumer PRs.
# shared-lib is in packages/shared-lib # consumer apps are in apps/app-a, apps/app-b # Make your change in packages/shared-lib git add packages/shared-lib/src/utils.ts git commit -m "feat(shared-lib): add parseConfig util" # One PR covers the library change # AND all usages -- no publish step needed # apps/app-a imports { parseConfig } from '@org/shared-lib' # No version number to bump. No npm publish.
# Step 1: bump in shared-lib repo cd shared-lib npm version minor # 2.1.0 -> 2.2.0 git add . && git commit -m "feat: add parseConfig util" git push origin main npm publish # publish to npm registry # Step 2: open a PR in app-a to consume 2.2.0 cd ../app-a npm update @org/shared-lib git add package.json package-lock.json git commit -m "chore: bump shared-lib to 2.2.0" # PR, review, merge # Step 3: repeat for app-b, app-c, ... # If apps are many, this is a 2-day process
Monorepo wins: single atomic PR vs multi-step publish cycle.
2. Rename a function used in 4 packages
Mono: single grep-and-rename PR. Poly: deprecate-publish-update-per-consumer cycle.
# Rename parseUserData -> parseUser across all packages # In your IDE or with sed/codemod: npx jscodeshift -t codemod-rename.js packages/ # Or with global search-replace: grep -rn "parseUserData" packages/ apps/ # Make the change everywhere git add -A git commit -m "refactor: rename parseUserData to parseUser across codebase" # One PR. One review. Atomic.
# Step 1: deprecate old name in shared-lib # Add @deprecated JSDoc, export alias # shared-lib: publish 2.3.0 # Step 2: update each consumer repo in sequence # app-a: PR to migrate from parseUserData -> parseUser # app-b: separate PR # app-c: separate PR # app-d: separate PR # Step 3: after all consumers migrated, # remove deprecated alias in shared-lib 3.0.0 # Total: 5 PRs, 3-4 review cycles, days to weeks # Risk: some consumers stay on deprecated name indefinitely
Monorepo wins: refactor in one commit vs deprecation-publish-update-remove cycle.
3. Add a new shared utility
Mono: import directly. Poly: create package, publish, version-bump consumers.
# Add the util to packages/utils/src/format.ts export function formatCurrency(n: number): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n); } # Use it immediately in any app: # apps/checkout/src/total.ts import { formatCurrency } from '@org/utils'; # No publish step. No version number. # Available immediately after the PR merges.
# Option A: add to existing shared-lib repo cd shared-lib # Write the util npm version patch # publish 2.3.1 npm publish # Then in each consumer repo: npm install @org/shared-lib@2.3.1 git commit -m "chore: bump shared-lib" # Option B: new package entirely mkdir format-utils && cd format-utils npm init -y # Set up build config, tsconfig, CI, README # 1-2 hours before you can write the actual util # Then publish + consumer updates
Monorepo wins: shared code is available immediately.
4. Coordinate a breaking API change
Mono: atomic change + build confirmation. Poly: contract test + staged rollout.
# Change the API signature in packages/api-client # TypeScript catches all consumers at compile time # Run affected check before merging: npx nx affected:build --base=origin/main # ✓ packages/api-client built # ✓ apps/app-a built (consumer) # ✓ apps/app-b built (consumer) # All compile errors resolved in the same PR. # The build gate is your contract test. # If it builds, the change is consistent.
# Step 1: bump API version in api-client repo # Add new method signature, deprecate old one # Publish api-client 3.0.0 (breaking) # Step 2: consumer migration (each repo separately) # - Update api-client version # - Find all usages of old API # - Test # - PR + review # Step 3: monitor for runtime failures # TypeScript might not catch everything across # separate compilation contexts # Step 4: deprecate old API version after 90 days # Publish 4.0.0 removing deprecated methods # Contract tests (Pact, OpenAPI) help but # cannot guarantee compile-time safety across repos
Monorepo wins: TypeScript compile-time validation across all consumers in one PR.
5. Run CI for a single-service change
Mono: needs 'affected' discipline. Poly: trivially isolated.
# Change in apps/billing only # With Nx affected or Turbo --filter: # Nx npx nx affected:build --base=origin/main # Runs: apps/billing (changed) # Skips: apps/checkout, apps/api, packages/* # (unless billing imports from them) # Turborepo npx turbo run build --filter=...[origin/main] # Same: only billing and its deps # Without affected configured: # Full rebuild = all 40 packages rebuild # This is the monorepo CI anti-pattern
# Change in billing repo only # CI is trivially isolated: # billing's CI pipeline runs # checkout's CI pipeline: not triggered # api's CI pipeline: not triggered # No configuration needed. # This is the default polyrepo behaviour. # Trade-off: if billing changes an API that # checkout depends on, checkout's CI doesn't # run. You only find the breakage when checkout # deploys and hits the updated billing API.
Polyrepo wins (by default). Monorepo wins with affected + proper caching. Without it, monorepo loses.
What the code samples show
Monorepo wins 4 of 5 tasks by default. The one task where polyrepo wins naturally (single-service CI isolation) is easily solved in a monorepo with affected builds configured. The reverse is not true: the monorepo advantages (atomic commits, compile-time cross-package type checking) cannot be replicated in polyrepo.
The catch: the affected build configuration must be in place. A monorepo without affected builds configured is worse than a polyrepo for task 5.