Skip to content
.md

Monorepos

Monorepos amplify every problem a merge queue solves. More developers, more PRs, more potential conflicts, more CI runs. A single queue becomes a bottleneck. Without one, main breaks constantly.

In a monorepo, the math works against you:

MetricPolyrepo (10 repos)Monorepo
PRs per day5 per repo = 50 total50 in one repo
Conflict potentialLow (isolated repos)High (shared codebase)
“Main is broken” impactOne team blockedEveryone blocked
CI costScales per repoFull suite every PR?

A broken main in a monorepo blocks everyone. The pressure to keep it green is much higher.

A naive merge queue serializes all PRs:

Frontend #1

Backend #2

iOS #3

Frontend #4

main

With 20-minute CI and 50 PRs/day, you’re underwater. PRs queue for hours. Engineers context-switch. Frustration mounts.

Parallel queues let independent changes merge simultaneously:

Frontend

Frontend

main

Backend

Backend

iOS

Android

Each team gets their own queue. Frontend changes don’t block backend. iOS doesn’t wait for Android.

Common approaches:

ApproachWorks well whenWatch out for
By directoryClear folder structure (apps/, libs/)Shared code in /common
By teamStrong code ownershipCross-team changes
By build targetUsing Bazel, Nx, TurborepoRequires build tool setup
By CI configDifferent test suites per areaConfig drift

Most teams start with directory-based rules and evolve from there.

The tricky part: what happens when a PR touches code used by multiple areas?

src/
├── apps/
│ ├── frontend/ → Frontend queue
│ ├── backend/ → Backend queue
│ └── mobile/ → Mobile queue
└── libs/
└── common/ → ??? which queue ???

The standard approach: the PR joins all affected queues and must pass all of them. A change to libs/common that’s used by frontend and backend joins both queues, runs both CI suites, and only merges when both pass.

Running the full test suite for every PR defeats the purpose. Monorepo-aware CI helps:

  • Build tools (Bazel, Nx, Turborepo, Pants) can determine affected tests
  • Per-queue CI — Each queue runs only its relevant tests
  • Two-step CI — Fast checks on PR, thorough checks in queue

Example queue CI strategy:

QueueCI Runs
FrontendFrontend unit tests, E2E, build
BackendBackend unit tests, API tests, migrations
MobileMobile unit tests, build for simulators
GlobalFull integration suite

Track how long PRs wait in each queue. If one queue consistently backs up:

  • Split it into sub-queues
  • Investigate slow CI
  • Add batching to reduce CI runs

Some changes genuinely need coordination. A breaking API change in backend needs frontend updates. Options:

  1. Stack PRs — Land backend first, then frontend
  2. Feature flags — Ship both independently, flag controls activation
  3. Atomic PR — Single PR touching both (goes to global queue)

Parallel queues give teams autonomy—each controls their merge pace. But this can lead to:

  • Different merge standards per team
  • Confusion about which queue a PR belongs to
  • Orphaned queues when teams restructure

Document queue ownership clearly. Review quarterly.

Stage 1: Single queue

  • Small team, fast CI
  • One queue works fine

Stage 2: CI becomes slow

Stage 3: Queue backs up despite optimization

  • Split into 2-3 parallel queues by major area
  • Keep a global queue for shared code

Stage 4: Multiple teams, clear ownership

  • Queue per team/domain
  • Build-tool integration for automatic assignment
  • Metrics dashboard per queue
  1. Monorepos need merge queues — The alternative is constant broken main
  2. Single queues don’t scale — Parallel queues are essential for large monorepos
  3. Start simple, evolve — Begin with directories, add sophistication as needed
  4. Shared code is the hard part — Have a clear strategy before it becomes a bottleneck
  5. Optimize CI per queue — Don’t run everything for every PR