Monorepo vs. Polyrepo: Making the Right Choice

October 31, 2022

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

The right answer depends on your context. Don’t copy Google or Netflix—analyze your own needs.