Practical guide to branching strategies, pull request flow, stacked PRs, merge queues, and CI collaboration in large teams.
If I were setting up how a large team ships through git today, what would I standardize before the first hundred PRs land?
This playbook focuses on:
- branching models and when each earns its place;
- branch protection and required checks;
- PR flow that scales past a handful of engineers;
- stacked PRs and how to land them;
- merge queues and why "green on branch" lies;
- rebase vs merge policy;
- conflict habits and CI collaboration;
- release branches and backports.
- Git Collaboration Playbook
- Table of Contents
- Companion playbooks
- Philosophy
- The defaults I'd reach for first
- Branching models
- Branch protection and required checks
- PR flow at scale
- Stacked PRs
- Merge queues
- Rebase vs merge policy
- Conflict habits
- CI collaboration
- Release branches and backports
- Things to avoid
- Worth reading
- License
These repositories form one playbook suite:
- AI-Assisted Engineering Playbook — agent workflows, guardrails, and quality control for AI-heavy teams
- API Design Playbook — versioning, pagination, idempotency, error contracts, and webhooks
- Auth & Identity Playbook — sessions, tokens, OAuth, and identity boundaries across the stack
- Backend Architecture Playbook — APIs, boundaries, OpenAPI, persistence, and errors
- Best of JavaScript — curated JS/TS tooling and stack defaults
- Caching Playbook — HTTP, CDN, and application caches; consistency and invalidation
- Code Review Playbook — PR quality, ownership, and review culture
- CTO Playbook — org design, hiring, technical strategy, budgets, and due diligence
- DevOps Delivery Playbook — CI/CD, environments, rollout safety, and observability
- Engineering Lead Playbook — standards, RFCs, and technical leadership habits
- Frontend Architecture Playbook — React structure, performance, and consuming API contracts
- Git Collaboration Playbook — branching, stacked PRs, merge queues, and CI collaboration at scale
- Marketing and SEO Playbook — growth, SEO, experimentation, and marketing surfaces
- Messaging & Async Playbook — queues, events, outbox, idempotent consumers, and retries
- Monorepo Architecture Playbook — workspaces, package boundaries, and shared code at scale
- Node.js Runtime & Performance Playbook — event loop, streams, memory, and production Node performance
- Observability Playbook — logs, traces, metrics, SLOs, and production visibility
- React Cross-Platform Playbook — shared React UI and logic across web and native with TypeScript
- Software Design Playbook — separation of concerns, composition, and module boundaries
- State Management Playbook — client state architecture, MobX, and choosing a state layer
- Styling Architecture Playbook — type-safe styling, design tokens, and theming at scale
- Sysadmin Operations Playbook — servers, backups, DNS, TLS, identity, and the self-hosted ops stack
- Testing Strategy Playbook — unit, integration, contract, E2E, and CI-friendly test layers
The best git workflow is not the cleverest one.
It is the one where:
- any engineer can describe the path from branch to production in one sentence;
- the default branch is always releasable;
- merging is boring, frequent, and automated;
- history reads as a sequence of reviewed, revertable units;
- nobody needs tribal knowledge to land a change.
The goal is not process. The goal is removing coordination as a daily tax.
Scope boundaries: how reviews are written and received lives in the Code Review Playbook; commit message format is its own topic (Conventional Commits); CI pipeline design and rollout safety live in the DevOps Delivery Playbook. This playbook is about how branches, PRs, and merges flow between people.
If I were setting up a team repository today, I would usually start with:
- Branching: trunk-based-ish — short-lived branches off the default branch, merged within days
- Merge method: squash merge for PRs, one commit per reviewed unit
- Protection: branch protection on the default branch from day one, admins included
- Merge queue: enabled once the team is large enough that PRs race each other
- Hygiene: auto-delete merged branches
- Auto-merge: on, so approved PRs land the moment checks pass
- History: linear, no merge commits on the default branch
Three models cover almost every team:
- Trunk-based development — everyone integrates into the default branch continuously, branches live hours to days, incomplete work hides behind feature flags
- GitHub Flow — branch, PR, review, merge, deploy; the default branch is always deployable
- Release branches — a stabilization branch cut per release, fixes cherry-picked back
In practice trunk-based and GitHub Flow converge: short-lived branches, PR review, fast merges. The real spectrum is branch lifetime, not model name.
- trunk-based: continuous deployment, strong CI, feature flags available
- GitHub Flow: the sensible default for web services and libraries
- release branches: versioned products, multiple supported versions, external release cadence you do not control
- a branch older than a week that is still "almost ready"
- a
developbranch that exists because it always has - integration branches that need their own integration branches
- merge day as a named event on the team calendar
A long-lived feature branch is a batch of unreviewed risk with a merge conflict attached. Break the work down instead — see Splitting large changes.
Protection rules are the contract that keeps the default branch releasable. My baseline:
- Required status checks. The checks that gate merging are the definition of "safe to ship". Choose them deliberately.
- Required reviews scaled by risk. One approval as default; two for auth, payments, migrations, and other blast-radius code — enforced via CODEOWNERS, not memory.
- Linear history required. Squash or rebase merges only; no merge commits on the default branch.
- Admins are not exempt. The moment admins can bypass, the rules are decorative. Emergencies go through the same gate, faster — not around it.
- Force pushes and deletions blocked on the default branch.
Which checks should be required is a CI design question — covered in the DevOps Delivery Playbook. The collaboration rule is simpler: required checks are few, fast, and trusted; everything else is advisory.
Small PRs are the single highest-leverage habit in this playbook.
- one coherent change per PR — reviewable in one sitting;
- a few hundred lines of meaningful diff as the soft ceiling;
- draft PRs for work-in-progress that wants early eyes without requesting review;
- the PR description states what changed and why, not a diff narration;
- open-to-merge time measured in hours or days, never weeks.
A PR that is hard to review gets a worse review. Review quality itself is the Code Review Playbook's territory.
- separate mechanical changes (renames, formatting, codemods) from behavioral ones
- land the refactor that enables the feature before the feature
- ship the schema or contract change ahead of the code that uses it
- if a split creates a dependency chain, that is a stack — see Stacked PRs
- update from the default branch when meaningfully behind, not on every push;
- a merge queue makes ritual pre-merge updates unnecessary — the queue tests against latest anyway;
- a branch that needs constant conflict resolution is telling you it has lived too long.
A stack is a chain of dependent PRs, each reviewable alone: PR 2 branches from PR 1, PR 3 from PR 2. Reach for it when:
- the change has natural layers (schema → API → UI)
- you would otherwise ship one 2,000-line PR
- you want to keep building while earlier parts sit in review
The alternative to a stack is usually a giant PR that gets a worse review, or a week of blocked waiting.
| Tool | Good when | Watch out for |
|---|---|---|
| Graphite | team-wide adoption, wants a full stacking workflow | a paid product; the team depends on it |
| git-spice | CLI-first, free, no server component | newer; smaller ecosystem |
plain gh + branches |
occasional stacks, no new tooling | manual restacking after every change below the top |
The mechanics are the same everywhere: each PR targets the branch below it; editing a lower branch means rebasing everything above. Tooling automates the restack; by hand it is painful past two levels.
- review bottom-up: the base PR first, since everything above depends on it;
- land bottom-up: merge PR 1, retarget PR 2 to the default branch, repeat;
- keep each PR independently green — a stack where only the top passes CI is one big PR wearing a costume;
- never merge the top of a stack ahead of its base.
A PR's checks run against the default branch as it was when the checks started. Two PRs can each be green, conflict-free, and still break the build when both land — a renamed function in one, a new caller in the other. Git sees no conflict; the compiler does.
At low merge frequency this is a rare annoyance. At high merge frequency it is a broken default branch several times a week, and every break blocks the whole team.
A merge queue fixes it structurally: PRs enter a queue, and CI runs against the actual future state of the branch — this PR plus everything queued ahead of it. GitHub's merge queue builds these speculative states in parallel, merges what passes in order, and ejects failures instead of landing them.
- the queue's answer is authoritative — no bypassing it because a PR "looks safe";
- queue throughput depends on check runtime — see check runtime budgets;
- a kicked-out PR is the system working.
A handful of engineers merging a few PRs a day rarely race each other. There, required checks plus auto-merge give the same safety with less machinery. The signal to adopt a queue is empirical: the default branch breaks from PR interleaving more than about once a month. Before that, a queue is ceremony.
My policy fits in four lines:
- squash merge is the default for landing PRs — one reviewed unit becomes one commit, revertable with
git revert; - rebase locally to update your own unshared branch — clean, linear, no merge-commit noise;
- never rewrite shared history — once a branch has a second person's work or a merged state, its history is fixed;
- merge commits are for the rare deliberate case (importing a subtree, preserving a collaborative branch), not the default.
Force-pushing your own PR branch is fine — it is your branch until it merges. Use --force-with-lease so you cannot clobber a push you have not seen.
Conflicts are not random weather. They are proportional to diff size times branch lifetime.
- small PRs and short-lived branches prevent most conflicts before they exist;
- rebase early — a conflict caught on day one is a two-minute fix, the same conflict on day ten is an afternoon;
- resolve conflicts on your branch, never on the default branch;
- lockfiles (
pnpm-lock.yaml,package-lock.json) are generated files: never hand-merge them — take either side, then re-run the install to regenerate; - the same file conflicting week after week is a boundary problem — covered in the Software Design Playbook.
CI is a shared resource and a shared contract. What I want:
- Required checks are the merge gate: few, fast, deterministic. Everything slow or advisory (nightly suites, coverage deltas, preview deploys) runs as optional and never blocks.
- Auto-merge on, so a PR lands the moment approval and checks align — nobody babysits a green PR waiting to press a button.
- Check runtime budgets: required checks in about ten minutes, hard ceiling fifteen. Past that, engineers batch changes into bigger PRs, and every rule in this playbook degrades.
- Failures on the default branch are the team's top priority — a red trunk blocks everyone.
Pipeline architecture — caching, parallelization, what runs where — is the DevOps Delivery Playbook's domain. Which tests exist at which layer is the Testing Strategy Playbook's.
A flaky required check is worse than no check: it trains the team to distrust red.
- quarantine known-flaky tests out of the required set immediately, with a tracking issue and an owner;
- never build a "re-run until green" culture — every ignored red makes the next real failure invisible;
- a re-run that flips the result is a defect report, not a fix;
- a required check that fails spuriously more than about 1% of runs loses its required status until repaired.
Most continuously deployed services do not need release branches. The default branch is the release.
They earn their place when:
- you ship versioned artifacts (SDKs, CLIs, mobile apps, on-prem software)
- multiple versions need support in parallel
- an external release calendar forces a stabilization window
Cherry-pick discipline when you have them:
- Fix on the default branch first, then cherry-pick to release branches — never the reverse, or the fix silently misses the next release.
- Cherry-pick the merged squash commit with
git cherry-pick -xso the backport records its origin. - Backports get PRs and required checks like any other change — a release branch is the least safe place for an unreviewed commit.
- Delete release branches when their version leaves support.
- long-lived feature branches as the standard unit of work;
- admins bypassing branch protection "just this once";
- 2,000-line PRs reviewed by scrolling speed;
- force-pushing shared branches;
- re-running flaky checks until green and calling it passing;
- required checks slow enough that people batch changes to avoid them;
- merge queues on a three-person team as cargo cult;
- treating the branching model as identity instead of a tool that must earn its cost.
- Trunk Based Development
- GitHub flow
- About protected branches
- Managing a merge queue
- About merge methods on GitHub
- Git Branching — Branching Workflows
- Conventional Commits
MIT is a sensible default for a repository like this, but choose the license that fits how you want others to reuse the material.