Should you put all your code in one repository or split it into many? This question sparks religious debates. The answer depends on your organization, tooling, and constraints. Both approaches work at massive scale—but require different investments.
Here’s how to make the right choice.
Understanding the Trade-offs
Monorepo Benefits
monorepo_benefits:
atomic_changes:
description: Change multiple packages in one commit
example: Update API and all clients together
benefit: No version coordination nightmares
code_sharing:
description: Easy to share code across projects
example: Common utilities, types, configs
benefit: Reduced duplication, consistency
single_source_of_truth:
description: One place for all code
example: Search across entire codebase
benefit: Discoverability, refactoring
unified_tooling:
description: One build system, one CI config
example: Shared linting, testing, deployment
benefit: Consistency, lower overhead
simplified_dependencies:
description: Internal dependencies just work
example: No publishing/versioning internal packages
benefit: Faster development velocity
Polyrepo Benefits
polyrepo_benefits:
clear_ownership:
description: Each repo has clear owner
example: Team owns their repo entirely
benefit: Autonomy, accountability
independent_lifecycle:
description: Release independently
example: Deploy without coordinating
benefit: Team velocity, isolation
access_control:
description: Fine-grained permissions
example: Contractors see only their repo
benefit: Security, compliance
simpler_tooling:
description: Standard Git workflows
example: No special build systems needed
benefit: Lower learning curve
smaller_blast_radius:
description: Problems isolated to repo
example: Broken CI doesn't affect others
benefit: Reliability, independence
The Trade-off Matrix
consideration_matrix:
coordination:
monorepo: Easy (same commit)
polyrepo: Hard (versions, compatibility)
independence:
monorepo: Hard (shared infrastructure)
polyrepo: Easy (full autonomy)
code_sharing:
monorepo: Trivial (just import)
polyrepo: Work required (publish packages)
build_complexity:
monorepo: High (need build system)
polyrepo: Low (standard tools)
ci_infrastructure:
monorepo: Heavy (smart caching needed)
polyrepo: Standard (per-repo CI)
onboarding:
monorepo: Clone once, everything available
polyrepo: Find repos, clone many
refactoring:
monorepo: Atomic across codebase
polyrepo: Coordinated releases
Monorepo Implementation
Build System Requirements
monorepo_tooling:
build_systems:
bazel:
strengths: Hermetic builds, excellent caching
complexity: High learning curve
scale: Google-scale proven
nx:
strengths: JavaScript ecosystem, good DX
complexity: Moderate
scale: Large JS projects
turborepo:
strengths: Simple, fast, good caching
complexity: Low
scale: Medium to large
pants:
strengths: Python-focused, good ergonomics
complexity: Moderate
scale: Medium to large
Affected-Based Testing
# Only test what changed
affected_testing:
principle: Only run tests for changed code and dependents
example:
changed: packages/auth
dependents:
- packages/api (imports auth)
- packages/web (imports auth)
run_tests:
- packages/auth
- packages/api
- packages/web
skip_tests:
- packages/billing (no dependency)
# Turborepo: Run affected tests
turbo run test --filter=...[origin/main]
# Nx: Run affected tests
nx affected:test --base=origin/main
# Bazel: Query affected targets
bazel query 'rdeps(//..., set(//packages/auth/...))'
Caching Strategy
build_caching:
local_cache:
purpose: Developer machine speed
implementation: Build system cache directory
hit_rate: High for incremental builds
remote_cache:
purpose: Share builds across team/CI
implementation: Cloud storage (S3, GCS)
hit_rate: High when code hasn't changed
distributed_execution:
purpose: Parallelize across machines
implementation: Build farm
use_case: Very large codebases
Polyrepo Implementation
Package Management
polyrepo_patterns:
internal_packages:
approach: Publish to private registry
tools: npm private registry, Artifactory, GitHub Packages
workflow: Version, publish, consume
dependency_management:
approach: Automated dependency updates
tools: Dependabot, Renovate
workflow: Bot creates PRs, team reviews
# Renovate config for internal packages
# renovate.json
{
"extends": ["config:base"],
"packageRules": [
{
"matchPackagePatterns": ["@company/*"],
"automerge": true,
"automergeType": "branch",
"schedule": ["before 6am on monday"]
}
]
}
Cross-Repo Changes
breaking_change_workflow:
step_1:
action: Add new API (backward compatible)
repo: provider-service
merge: true
step_2:
action: Migrate to new API
repo: consumer-service-1, consumer-service-2
merge: true
step_3:
action: Remove old API
repo: provider-service
merge: true
challenge: Coordination across multiple PRs
mitigation: Strong API versioning, deprecation policy
Hybrid Approaches
Strategic Monorepos
hybrid_approach:
description: Multiple monorepos by domain or team
structure:
platform_monorepo:
contains: Infrastructure, shared libraries
teams: Platform engineering
product_monorepo:
contains: Product services, frontends
teams: Product engineering
data_monorepo:
contains: Data pipelines, ML models
teams: Data engineering
benefits:
- Domain cohesion within repos
- Independence between domains
- Manageable scale
Shared Libraries
shared_library_strategy:
monorepo_approach:
location: Same repo as services
versioning: None (always use latest)
breaking_changes: Fix all consumers atomically
polyrepo_approach:
location: Separate library repos
versioning: Semantic versioning
breaking_changes: Coordinated migration
Decision Framework
Choose Monorepo When
monorepo_indicators:
tight_coupling:
- Frequent cross-service changes
- Shared types and interfaces
- API changes need client updates
small_to_medium_team:
- Everyone works on everything
- Full-stack ownership
- High collaboration
willing_to_invest:
- Custom build tooling acceptable
- CI infrastructure investment
- Training on new tools
prioritize:
- Consistency over autonomy
- Coordination over independence
- Simplicity of dependencies
Choose Polyrepo When
polyrepo_indicators:
loose_coupling:
- Services communicate via stable APIs
- Independent release cycles
- Minimal cross-service changes
large_distributed_teams:
- Team autonomy important
- Different tech stacks
- Compliance/security isolation needed
standard_tooling_preferred:
- Minimal custom infrastructure
- Use standard CI/CD
- Lower learning curve
prioritize:
- Autonomy over coordination
- Independence over consistency
- Simplicity of infrastructure
Migration Paths
Polyrepo to Monorepo
migration_to_monorepo:
approach: Gradual migration
steps:
1_setup:
- Create monorepo structure
- Setup build system
- Configure CI/CD
2_migrate_leaves:
- Start with repos with few dependents
- Move code, preserve history
- Update CI to build from monorepo
3_migrate_core:
- Move shared libraries
- Update all imports
- Delete old repos
timeline: 3-6 months typical
Monorepo to Polyrepo
migration_to_polyrepo:
approach: Extract and publish
steps:
1_define_boundaries:
- Identify service boundaries
- Map dependencies
- Define API contracts
2_extract_packages:
- Create separate repos
- Publish shared libraries
- Update imports to packages
3_split_services:
- Move services to own repos
- Setup independent CI/CD
- Remove from monorepo
timeline: 3-6 months typical
Key Takeaways
- Both monorepo and polyrepo work at scale—with different investments
- Monorepo: Better for tight coupling, high coordination needs
- Polyrepo: Better for team autonomy, loose coupling
- Monorepo requires build system investment (Bazel, Nx, Turborepo)
- Polyrepo requires package management and coordination practices
- Hybrid approaches often work best: domain-aligned monorepos
- Migration is possible in either direction
- Choose based on your team, coupling, and tooling appetite
- The worst choice is no choice—scattered repos with no strategy
The right answer depends on your context. Don’t copy Google or Netflix—analyze your own needs.