Feature Branches in Large/Monorepo Repositories

Published on
Written byChristoffer Artmann
Feature Branches in Large/Monorepo Repositories

Your monorepo contains 50 services, 200 packages, and contributions from 15 teams across three time zones. A feature branch that touches the shared authentication library potentially affects every service. Another branch modifies just one microservice but needs to coordinate with infrastructure changes happening in parallel. Someone else's branch has been open for two weeks, blocking three other features that depend on it.

This is feature branch management at scale. The patterns that work beautifully for a five-person team working on a single application break down when you're coordinating across teams, services, and deployment boundaries in a single repository.

Large repositories and monorepos introduce challenges that smaller codebases never encounter: coordinating changes across teams, managing dependencies between services, handling deployment complexity, and keeping CI/CD pipelines fast when the codebase is massive. The solution isn't abandoning feature branches—it's adapting the workflow to handle scale.

The Unique Challenges of Monorepo Feature Branches

Feature branches in monorepos face problems that don't exist in smaller repositories.

Dependency Coordination

In a monorepo, packages depend on each other. A feature branch that updates a shared library's interface breaks every service that uses that library. You can't merge until you've updated all consumers—but those consumers might be owned by different teams who don't know your changes are coming.

Consider this scenario: Team A wants to add authentication tokens to the API client library. This change requires updates in 15 services that use the library. Some of those services are maintained by Team B, who's focused on their own deadlines. Others are legacy services with unclear ownership.

Without coordination, Team A's feature branch grows enormous—thousands of lines changing dozens of files across multiple services. The PR becomes difficult to review, conflicts accumulate daily, and merging becomes risky. The longer it takes, the more conflicts pile up, creating a vicious cycle.

CI/CD Performance at Scale

When every feature branch runs the full test suite for all 50 services, pipelines take hours. Developers push changes in the morning and don't get feedback until afternoon. This destroys the tight feedback loop that makes feature branches effective.

Running only affected tests helps, but computing affected packages has its own complexity. Change a shared utility function and suddenly 40 packages are affected. The CI system needs to build dependency graphs, determine what tests to run, and execute them efficiently—all while multiple feature branches compete for resources.

Merge Conflicts and Churn

In active monorepos, main changes constantly. During the two weeks your feature branch lives, main might receive 200 commits from 30 different developers. Keeping your branch current requires frequent merges or rebases, each potentially introducing conflicts.

The conflict rate increases with repository size and team size. More developers making more changes means higher probability that someone touched the same code you're modifying. In a five-person team, you might encounter conflicts weekly. In a 50-person team, you might hit them daily.

Deployment Coordination

Many monorepos contain multiple deployable services. Your feature branch might change three services that deploy independently. Do you deploy them together? Separately? What if Service A's changes depend on Service B's changes landing first?

Feature branches that span multiple services need deployment orchestration. You can't simply merge and deploy—you need to ensure services deploy in the right order, feature flags coordinate behavior across services, and rollback procedures work when services are interdependent.

Scaling Feature Branch Workflows

Effective monorepo workflows adapt traditional feature branch patterns to handle these challenges.

Scope-Limited Branches

Instead of massive branches that touch everything, create focused branches that change one service or package:

# Bad: Enormous branch touching 15 services
feature/add-auth-tokens  # Changes 15 services, 120 files

# Good: Separate branches for each piece
feature/api-client-auth-tokens  # Changes shared library
feature/user-service-auth-tokens  # Updates one service
feature/payment-service-auth-tokens  # Updates another service

This breaks large changes into manageable pieces. Each branch can merge independently once its piece is complete. Services adopt the new library interface gradually rather than all at once.

Yes, this means more coordination and more PRs, but each PR is reviewable, testable, and mergeable without coordinating 15 teams.

CODEOWNERS for Routing Reviews

Monorepos with many teams need automatic review routing. CODEOWNERS files specify which teams own which parts of the codebase:

# .github/CODEOWNERS

# API shared libraries
/packages/api-client/ @team-platform
/packages/auth/ @team-security

# Services
/services/user-service/ @team-identity
/services/payment-service/ @team-payments
/services/shipping-service/ @team-logistics

# Infrastructure
/infrastructure/ @team-sre
/scripts/deploy/ @team-sre

# Documentation
/docs/ @team-platform @team-tech-writers

When someone opens a PR touching packages/auth/, GitHub automatically requests review from @team-security. This ensures the right people review changes without manual routing.

For changes spanning multiple areas, multiple teams get requested automatically:

# PR changes files in three directories
- packages/auth/token-validator.ts
- services/user-service/auth-handler.ts
- infrastructure/auth-service.yaml

# Automatic review requests to:
- @team-security (for auth package)
- @team-identity (for user service)
- @team-sre (for infrastructure)

Affected Package Testing

Computing and testing only affected packages dramatically reduces CI time:

# Using Nx for affected testing
test-affected:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0 # Need full history for affected analysis

    - name: Compute affected packages
      run: |
        npx nx affected:apps --base=origin/main --head=HEAD

    - name: Test affected packages
      run: |
        npx nx affected:test --base=origin/main --head=HEAD --parallel=3

    - name: Build affected packages
      run: |
        npx nx affected:build --base=origin/main --head=HEAD --parallel=3

This approach only tests packages that changed or depend on changed packages. A feature branch that modifies one service might run 5% of tests instead of 100%, cutting CI time from hours to minutes.

Branch Naming with Service Prefixes

In monorepos, branch names should indicate which services they affect:

# Service-specific changes
user-service/feature/profile-page
payment-service/bugfix/currency-rounding

# Shared library changes
api-client/feature/request-retry
auth/refactor/token-validation

# Cross-cutting changes
platform/feature/logging-improvements

# Infrastructure changes
infra/chore/update-terraform

This makes it immediately clear which teams care about each branch and helps with filtering CI jobs, automating deployments, and understanding the scope of changes.

Managing Dependencies Between Feature Branches

When features depend on each other, coordinating branches becomes critical.

Stacking Feature Branches

Sometimes Feature B depends on Feature A. Instead of waiting for A to merge, stack branches:

main
  ↓
feature/A (base: main)
  ↓
feature/B (base: feature/A)

Branch B builds on Branch A's changes. When A merges, rebase B onto main:

# After feature/A merges
git checkout feature/B
git rebase --onto main feature/A

This flattens the stack, removing A's commits (now in main) and leaving only B's commits.

Stacking lets dependent work proceed in parallel. Team A works on their feature, Team B works on their feature that needs A's changes, and both features progress simultaneously.

Feature Flag Coordination

For changes that require coordination across services, use feature flags:

// Service A: New behavior behind flag
if (featureFlags.get('new-auth-flow')) {
  return await newAuthenticationFlow(request)
} else {
  return await legacyAuthenticationFlow(request)
}

// Service B: Consumes new flow when enabled
const authClient = new AuthClient({
  useNewFlow: featureFlags.get('new-auth-flow')
})

All services merge their feature branches with flags disabled. Once everything is deployed, enable the flag to activate the new behavior across all services simultaneously. This decouples deployment from feature activation.

Dependency Version Management

When updating shared library versions in a monorepo, use workspace dependencies:

// packages/user-service/package.json
{
  "dependencies": {
    "@mycompany/api-client": "workspace:*"
  }
}

The workspace:* protocol ensures services always use the current version of shared libraries from the monorepo, eliminating version mismatch issues during development.

Branch Protection at Scale

Monorepos need sophisticated branch protection rules to maintain quality without slowing teams down.

Required Checks Based on Changed Files

Run different checks for different parts of the monorepo:

# Only run user-service tests when user-service changes
test-user-service:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Check for user-service changes
      id: filter
      uses: dorny/paths-filter@v2
      with:
        filters: |
          user-service:
            - 'services/user-service/**'

    - name: Test user-service
      if: steps.filter.outputs.user-service == 'true'
      run: npm test --workspace=user-service

This prevents running unrelated tests. A documentation change doesn't need to run the payment service test suite.

Conditional Required Status Checks

GitHub allows marking checks as required only when they run:

Branch protection rules:
☑ Require status checks to pass
☑ Require status checks for changed files only
  - test-user-service (required if runs)
  - test-payment-service (required if runs)
  - test-api-client (required if runs)

This means each PR only requires checks for the services it actually modifies.

Team-Specific Protection Rules

Different teams may need different rules. GitHub allows per-team bypass of certain protections:

main branch protection:
☑ Require pull request reviews (2 approvals)
☑ Allow specified actors to bypass (Platform Team)

release/* branches protection:
☑ Require pull request reviews (3 approvals)
☑ Require review from Code Owners
☑ No bypass allowed

This lets platform teams move quickly on infrastructure changes while maintaining stricter controls for releases.

Deployment Strategies for Monorepo Services

Deploying from feature branches in monorepos requires coordination across services.

Service-Specific Preview Environments

Deploy only changed services to preview environments:

deploy-preview:
  steps:
    - name: Determine changed services
      id: changes
      run: |
        services=$(npx nx affected:apps --plain --base=origin/main)
        echo "services=$services" >> $GITHUB_OUTPUT

    - name: Deploy affected services
      run: |
        for service in ${{ steps.changes.outputs.services }}; do
          echo "Deploying $service to preview"
          ./scripts/deploy-preview.sh $service ${{ github.head_ref }}
        done

This deploys only the services that changed, avoiding unnecessary deployments and reducing preview environment costs.

Canary Deployments Per Service

In monorepos with microservices, canary individual services rather than the entire system:

deploy-canary-user-service:
  if: contains(github.event.pull_request.labels.*.name, 'canary-user-service')
  steps:
    - name: Deploy user-service to 5% of production
      run: |
        ./scripts/deploy-canary.sh user-service 5%

    - name: Monitor for 1 hour
      run: |
        ./scripts/monitor-metrics.sh user-service 3600

    - name: Promote or rollback
      run: |
        if ./scripts/check-health.sh user-service; then
          ./scripts/promote-canary.sh user-service
        else
          ./scripts/rollback-canary.sh user-service
        fi

This allows testing risky changes in production with minimal blast radius. Only users served by the canary see the new code, and automatic rollback prevents widespread issues.

Progressive Service Rollouts

For changes spanning multiple services, roll them out progressively:

Day 1: Deploy service-a (with feature flag off)
Day 2: Deploy service-b (with feature flag off)
Day 3: Deploy service-c (with feature flag off)
Day 4: Enable feature flag at 5%
Day 5: Enable feature flag at 25%
Day 6: Enable feature flag at 100%

This staged approach isolates deployment issues from feature issues and provides multiple rollback points.

Performance Optimization for Large Codebases

Monorepos require optimization to keep feature branch workflows fast.

Partial Clones and Sparse Checkouts

Reduce clone time with partial clones:

# Clone only recent history
git clone --depth=1 https://github.com/company/monorepo

# Sparse checkout: only checkout specific services
git sparse-checkout init --cone
git sparse-checkout set services/user-service packages/api-client

This dramatically reduces clone and checkout time for large repos. Developers working on one service don't need to fetch the entire history of every service.

Build Caching Across Branches

Share build caches between branches to speed up CI:

- name: Setup build cache
  uses: actions/cache@v3
  with:
    path: |
      node_modules/.cache
      .nx/cache
    key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-build-
      ${{ runner.os }}-

The first feature branch builds everything and caches it. Subsequent branches restore from cache, building only what changed.

Distributed Task Execution

Tools like Nx Cloud distribute tasks across multiple machines:

- name: Setup Nx Cloud
  run: npx nx-cloud start-ci-run

- name: Run affected tests
  run: npx nx affected:test --parallel=5 --ci

This spreads test execution across available agents, dramatically reducing total CI time for large test suites.

Communication and Coordination Patterns

Technical solutions only go so far. Monorepos require strong communication patterns.

RFC Process for Major Changes

For changes affecting multiple teams, write RFCs (Request for Comments) before creating feature branches:

# RFC: Migrate to new authentication library

## Summary

Migrate from legacy-auth to modern-auth across all services.

## Affected Teams

- @team-identity (owns 3 services)
- @team-payments (owns 2 services)
- @team-platform (owns auth library)

## Migration Plan

1. Update auth library interface (Platform team)
2. Update high-traffic services (Identity, Payments)
3. Update low-traffic services (all teams)
4. Deprecate legacy library

## Timeline

- Weeks 1-2: RFC review and library updates
- Weeks 3-6: Service migrations
- Week 7: Final deprecation

This builds consensus before code is written, preventing surprises when large feature branches appear.

Slack Channels for Coordination

Create channels for coordinating complex changes:

#feat-auth-migration
  - Status updates
  - Blocker discussion
  - Review requests
  - Deployment coordination

This gives everyone working on related features a shared space for coordination.

Branch Status Dashboard

Build dashboards showing feature branch status:

Current Feature Branches by Team:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Team Identity:
  feature/profile-redesign (7 days old, CI passing, needs review)
  feature/email-verification (2 days old, CI passing, approved)

Team Payments:
  feature/stripe-migration (14 days old, CI passing, blocked on infra)
  bugfix/currency-rounding (1 day old, CI passing, needs review)

Team Platform:
  feature/logging-v2 (21 days old, CI failing, needs rebase)

This visibility helps teams coordinate and identify branches that need attention.

The Connection to Better Monorepo Management

Feature branches in monorepos require more discipline and tooling than in smaller codebases, but the investment pays off. When done well, monorepos provide the benefits of code reuse, atomic cross-service changes, and unified tooling while maintaining the agility of feature branch development.

At Pull Panda, we work with teams managing codebases of all sizes. We've seen that successful monorepo development depends on providing clear context for reviewers. When reviewing a change that touches five services, reviewers need to understand the scope, see test results for all affected services, and evaluate changes in the context of the broader system. Feature branches that provide this context enable effective code review even at monorepo scale.

For more on feature branch fundamentals that apply regardless of repository size, see our complete guide to mastering feature branches. And to understand how CI/CD integration helps manage monorepo complexity, check out our article on feature branches and CI/CD.