Back to Software Engineering & Delivery Mastery Series

Part 11: Advanced Git Workflows — GitFlow, Trunk-Based & Monorepos

May 13, 2026 Wasil Zafar 42 min read

Your branching strategy determines your deployment frequency. This article gives you the complete decision framework for choosing between GitFlow, GitHub Flow, Trunk-Based Development, and monorepo strategies — with real-world tradeoffs and team-size guidance.

Table of Contents

  1. Introduction
  2. GitFlow
  3. GitHub Flow
  4. GitLab Flow
  5. Trunk-Based Development
  6. Comparison Table
  7. Monorepos vs Polyrepos
  8. Branch Protection & Policies
  9. Case Studies
  10. Exercises
  11. Conclusion & Next Steps

Introduction — Why Branching Strategies Matter

If you can only change one thing about your engineering organisation's delivery speed, change the branching strategy. Research from the DORA team (Accelerate, 2018) consistently shows that deployment frequency correlates strongly with team performance — and your branching model is the single biggest constraint on how often you can deploy.

A branching strategy is your team's agreement about when code splits from the main line, how long it lives in isolation, and what conditions must be met before it merges back. Get this wrong and you'll face merge conflicts measured in days, releases delayed by weeks, and hotfixes that break other features.

Key Insight: There is no universally "best" branching strategy. The right choice depends on your deployment model, team size, compliance requirements, and release cadence. This article gives you the framework to choose correctly.

The Relationship Between Branching and Deployment Frequency

Consider this spectrum: at one extreme, a team using full GitFlow with release branches might deploy every 2-4 weeks. At the other extreme, a team using trunk-based development deploys dozens of times per day. Both can be correct — for different contexts.

Branching Model vs Deployment Frequency Spectrum
flowchart LR
    A[GitFlow] --> B[GitLab Flow]
    B --> C[GitHub Flow]
    C --> D[Trunk-Based Dev]

    A --- E["Deploy: 2-4 weeks"]
    B --- F["Deploy: 1-2 weeks"]
    C --- G["Deploy: Daily"]
    D --- H["Deploy: Multiple/day"]

    style A fill:#132440,color:#fff
    style B fill:#16476A,color:#fff
    style C fill:#3B9797,color:#fff
    style D fill:#BF092F,color:#fff
                            

The critical question isn't "which strategy is best?" but rather "what does your business need?" A medical device company with FDA compliance needs GitFlow's rigour. A startup shipping a SaaS product needs trunk-based development's speed.

GitFlow — The Classic Enterprise Model

GitFlow is a branching model — a set of rules that prescribe which branches exist, what each branch is for, and how code moves between them. It was designed to manage software that ships discrete, versioned releases (v1.0, v2.0, v2.1.1) rather than software that deploys continuously.

Vincent Driessen published "A successful Git branching model" in 2010, and it became the default for enterprise teams. The core idea is separation of concerns by branch: stable released code lives on main, in-progress development accumulates on develop, individual features are isolated on short-lived feature branches, and the release stabilisation process happens on dedicated release branches — so new feature work never interferes with release hardening.

Mental Model: Think of GitFlow as a factory assembly line with separate lanes. Raw materials (features) enter on one lane, get assembled on another (develop), undergo quality inspection on a third (release branch), and exit as finished goods on the shipping lane (main). Urgent recalls (hotfixes) bypass the normal flow with a direct lane from shipping back to the factory floor.

GitFlow defines five branch types with strict rules about how code flows between them.

The Five Branch Types

Branch Lifetime Purpose Created From Merges Into
main Permanent Production-ready code
develop Permanent Integration branch main release, main
feature/* Temporary New feature work develop develop
release/* Temporary Release preparation develop main + develop
hotfix/* Temporary Production bug fixes main main + develop
GitFlow Branch Lifecycle
gitGraph
    commit id: "init"
    branch develop
    commit id: "dev-1"
    branch feature/login
    commit id: "feat-1"
    commit id: "feat-2"
    checkout develop
    merge feature/login id: "merge-feat"
    branch release/1.0
    commit id: "bump-version"
    commit id: "fix-typo"
    checkout main
    merge release/1.0 id: "v1.0" tag: "v1.0.0"
    checkout develop
    merge release/1.0 id: "back-merge"
    checkout main
    branch hotfix/1.0.1
    commit id: "critical-fix"
    checkout main
    merge hotfix/1.0.1 id: "v1.0.1" tag: "v1.0.1"
    checkout develop
    merge hotfix/1.0.1 id: "hotfix-merge"
                            

When GitFlow Works

GitFlow excels in specific contexts:

  • Multiple supported versions — If you maintain v2.x and v3.x simultaneously (desktop software, mobile SDKs, embedded firmware), GitFlow's release branches are essential
  • Scheduled releases — If your organisation releases on fixed cadences (quarterly, monthly), the release branch gives a stabilisation period
  • Regulatory compliance — When each release requires sign-off, audit trails, and traceability, GitFlow's explicit branching makes compliance documentation straightforward
  • Large teams with siloed features — When 50+ developers work on independent features that must be batched into releases

Common Pitfalls

Warning: GitFlow is not suitable for continuous deployment. If you deploy from main multiple times per day, the develop branch becomes an unnecessary bottleneck. Vincent Driessen himself added a note to his original post recommending simpler models for web applications.

The most common GitFlow failure is long-lived feature branches. When a feature branch lives for 3+ weeks, it diverges so far from develop that merging becomes a multi-day project. The solution — if you must use GitFlow — is to keep feature branches under 5 days and rebase frequently.

GitFlow in Practice — Step by Step

Let's walk through a typical day for a developer using GitFlow. Imagine you're adding a user authentication feature to an e-commerce application. In GitFlow, you always branch from develop (never from main) because develop contains the latest approved work that hasn't been released yet. Here's the complete lifecycle of a feature branch:

# ===================================================
# GitFlow: Complete Feature Branch Lifecycle
# ===================================================
# CONTEXT: You're adding user authentication to an
# e-commerce app. Your team uses GitFlow, so all new
# feature work branches off 'develop'.
# ===================================================

# Step 1: Start from the latest develop branch
# (Always pull first to ensure you're not behind)
git checkout develop
git pull origin develop

# Step 2: Create a feature branch with a descriptive name
# Convention: feature/[ticket-number]-short-description
git checkout -b feature/user-authentication

# Step 3: Work on the feature (multiple commits are fine)
git add src/auth/jwt-validator.ts
git commit -m "feat: add JWT token validation middleware"

git add src/auth/login-handler.ts
git commit -m "feat: implement login endpoint with bcrypt"

# Step 4: Stay up-to-date with develop (do this DAILY)
# This prevents painful mega-merges at the end
git fetch origin
git rebase origin/develop
# If conflicts occur, resolve them now while they're small

# Step 5: When the feature is complete and tested, merge back
# The --no-ff flag preserves the feature branch in history
# (creates a merge commit even if fast-forward is possible)
git checkout develop
git merge --no-ff feature/user-authentication
git push origin develop

# Step 6: Clean up — delete the branch (it's merged)
git branch -d feature/user-authentication
git push origin --delete feature/user-authentication

When enough features accumulate on develop and the team is ready to ship, a release branch is created. Think of this as a "feature freeze" — no new features are added, only bug fixes and version bumps. This gives QA time to test without new code landing underneath them:

# ===================================================
# GitFlow: Complete Release Branch Lifecycle
# ===================================================
# CONTEXT: Your team has finished 5 features on develop
# and is ready to ship version 2.1.0. You create a release
# branch to stabilise the code before it goes live.
# ===================================================

# Step 1: Create the release branch from develop
# This "freezes" the feature set for this release
git checkout develop
git checkout -b release/2.1.0

# Step 2: Bump version numbers in all relevant files
echo "2.1.0" > VERSION
git add VERSION
git commit -m "chore: bump version to 2.1.0"

# Step 3: Fix ONLY release-critical bugs here
# New features go to develop for the NEXT release
git commit -m "fix: correct pagination off-by-one error"
git commit -m "fix: handle null user in profile endpoint"

# Step 4: When ready to ship, merge into BOTH main AND develop
# Merge to main (this IS the release)
git checkout main
git merge --no-ff release/2.1.0
git tag -a v2.1.0 -m "Release 2.1.0 — user auth + dashboard"
git push origin main --tags

# Merge back to develop (so develop gets the bug fixes)
git checkout develop
git merge --no-ff release/2.1.0
git push origin develop

# Clean up
git branch -d release/2.1.0

GitHub Flow — The Simplified Model

GitHub Flow is a dramatically simpler alternative to GitFlow, created by GitHub's own engineering team in 2011. Where GitFlow has five branch types with complex rules, GitHub Flow has just two concepts: a permanently deployable main branch, and short-lived feature branches.

The philosophy behind GitHub Flow is radical simplicity: if main is always deployable, you don't need release branches. If you deploy immediately after every merge, you don't need a separate develop branch. If bugs are fixed the same way as features (branch → PR → merge → deploy), you don't need hotfix branches either.

Analogy: If GitFlow is like a formal restaurant with multiple courses, strict timing, and assigned seating — GitHub Flow is like a food truck. You order (create a branch), you get your food (write code), it's quality-checked (PR review), and you eat (deploy). Simple, fast, and efficient when you don't need the ceremony.

GitHub Flow strips branching down to its essence: there is main (always deployable) and feature branches (short-lived work). That's it. No develop branch. No release branches. No hotfix branches.

GitHub Flow — Simple and Direct
gitGraph
    commit id: "init"
    branch feature/search
    commit id: "search-1"
    commit id: "search-2"
    checkout main
    merge feature/search id: "PR #42"
    commit id: "deploy-1" tag: "deployed"
    branch feature/dark-mode
    commit id: "dark-1"
    checkout main
    branch fix/typo
    commit id: "typo-fix"
    checkout main
    merge fix/typo id: "PR #43"
    commit id: "deploy-2" tag: "deployed"
    merge feature/dark-mode id: "PR #44"
    commit id: "deploy-3" tag: "deployed"
                            

The GitHub Flow Process

  1. Create a branch from main with a descriptive name
  2. Add commits — small, atomic, well-described
  3. Open a Pull Request — start discussion, request review
  4. Review and discuss — CI runs tests automatically
  5. Merge — squash or merge commit into main
  6. Deploy — main is always deployable; deploy immediately

Let's see how this looks in practice. Notice how much simpler the commands are compared to GitFlow — there's no switching between develop and main, no release branches to manage, and no back-merging:

# ===================================================
# GitHub Flow: The Complete Workflow
# ===================================================
# CONTEXT: You're adding a search API to a SaaS
# application. Your team deploys multiple times per
# day, so there's no need for release branches.
# ===================================================

# Step 1: Create a branch directly from main
# (In GitHub Flow, main is ALWAYS the source of truth)
git checkout main
git pull origin main
git checkout -b feature/add-search-api

# Step 2: Make small, focused commits
# Each commit should be a logical unit of work
git add src/search.ts
git commit -m "feat: implement search endpoint with Elasticsearch"

git add tests/search.test.ts
git commit -m "test: add search endpoint integration tests"

# Step 3: Push your branch and open a Pull Request (PR)
# The PR is where discussion, review, and CI happen
git push origin feature/add-search-api

# Open PR via GitHub CLI (or use the GitHub web interface)
gh pr create --title "feat: add search API" \
    --body "Implements full-text search using Elasticsearch" \
    --reviewer @team-lead

# Step 4: CI runs automatically on the PR
# Tests, linting, and build checks must pass
# Teammates review the code and leave comments

# Step 5: After approval, merge via GitHub UI or CLI
# Squash merge combines all commits into one clean commit
gh pr merge --squash

# Step 6: main is deployed to production AUTOMATICALLY
# Your CI/CD pipeline picks up the new commit on main
# and deploys it — typically within minutes

GitHub Flow works best for:

  • Web applications and SaaS — where there's only one version in production
  • Small to medium teams (2-20 developers)
  • Continuous deployment — deploying multiple times per day
  • Products without multiple supported versions
Industry Example GitHub Engineering Team

GitHub's Own Deployment Practice

GitHub itself uses GitHub Flow (naturally). Their engineering team deploys to production dozens of times per day. Every merged PR triggers a deployment pipeline that includes automated tests, canary deployment, and progressive rollout. The average time from merge to production is under 15 minutes.

Key enabler: comprehensive automated testing (>50,000 tests) combined with feature flags for incomplete work. Engineers can merge code that isn't customer-visible yet.

continuous deployment feature flags progressive rollout

GitLab Flow — The Middle Ground

GitLab Flow was introduced by GitLab in 2014 as a pragmatic compromise between GitFlow's complexity and GitHub Flow's simplicity. It solves a real-world problem that GitHub Flow ignores: what happens when you can't deploy every merge to production immediately?

Many organisations have regulatory requirements, staging environments that need manual QA sign-off, or customer-facing demo environments that must stay stable. In these cases, GitHub Flow's "merge to main = deploy to production" model breaks down. But GitFlow's five-branch overhead is still too heavy.

Analogy: If GitFlow is a full assembly line and GitHub Flow is a food truck, then GitLab Flow is like a restaurant with a kitchen (main), a pass (staging), and table service (production). Food moves from kitchen → pass → table in a controlled sequence, but without the rigid formality of a multi-course tasting menu.

GitLab Flow sits between GitFlow's complexity and GitHub Flow's simplicity. It introduces environment branches that map to deployment environments. Developers work exactly like GitHub Flow (branch from main, open MR, merge to main), but then code is promoted through environment branches in order:

Environment Branches

In GitLab Flow, code flows downstream through environment branches. Each branch corresponds to a real deployment target. Merging into an environment branch triggers deployment to that environment. Here's a typical promotion flow from development through staging to production:

# ===================================================
# GitLab Flow: Environment Branch Promotion
# ===================================================
# CONTEXT: Your company has a staging environment
# where QA validates changes before production.
# You can't deploy every merge immediately — changes
# must pass staging validation first.
# ===================================================

# Step 1: Developers merge to main (like GitHub Flow)
# This is where day-to-day development happens
git checkout main
git merge --no-ff feature/new-dashboard
# At this point, the feature is on main but NOT in staging or production

# Step 2: Promote main → staging
# This can be manual ("we're ready to test") or automated
# Merging into 'staging' triggers a deployment to the staging server
git checkout staging
git merge main
git push origin staging
# → CI/CD pipeline deploys to https://staging.yourapp.com

# Step 3: QA validates on staging...
# (manual testing, stakeholder review, etc.)

# Step 4: After staging validation, promote to production
# Merging into 'production' triggers the production deployment
git checkout production
git merge staging
git push origin production
# → CI/CD pipeline deploys to https://yourapp.com

# KEY INSIGHT: Code ONLY moves forward (main → staging → production)
# You never merge backward (production → staging)
# This ensures every environment is a subset of the one before it

Comparison with Other Flows

Aspect GitFlow GitHub Flow GitLab Flow
Long-lived branches main + develop main only main + environment branches
Release branches Yes (mandatory) No Optional
Deploy trigger Merge to main Merge to main Merge to environment branch
Staging environment Ad-hoc Feature branch deploys Dedicated branch
Best for Versioned releases Continuous deployment Regulated environments

Trunk-Based Development

Trunk-Based Development (TBD) is the most radical simplification of all branching strategies. The word "trunk" is an older term for the main branch (from Subversion/CVS days) — think of it as the trunk of a tree from which all branches grow. In TBD, everyone commits directly to main (the "trunk"), or uses feature branches that live for less than one day.

There is no develop branch, no release branches, no integration branches. Main is always deployable because it's always being deployed. If that sounds terrifying, you're not alone — most developers' first reaction is "won't that break everything?" The answer is no, but only if you have the right safety nets in place.

Key Insight: Trunk-Based Development isn't reckless — it's disciplined. It requires the highest level of engineering maturity: comprehensive automated tests, feature flags, pair programming or rapid code review, and CI that runs in under 10 minutes. The apparent risk ("what if someone pushes broken code?") is mitigated by making breakage immediately visible and instantly fixable.
Definition — Feature Flag: A feature flag (also called a feature toggle) is a conditional statement in your code that shows or hides functionality based on a configuration value. It allows you to merge incomplete or experimental code into main without users ever seeing it. The code is deployed but not released — a critical distinction in trunk-based development.

Feature Flags for Incomplete Work

The apparent contradiction — "how do you merge unfinished features into main?" — is solved by feature flags. Code exists in main but is invisible to users until the flag is enabled. Think of it like building a new room in a house: the room is constructed (code is in main) but the door is locked (feature flag is off). Guests (users) don't even know it's there until you unlock it.

Here's a real-world example of how feature flags work in a checkout flow. Notice that the new checkout code is deployed alongside the old code, but which version users see is controlled by an environment variable:

// Feature flag pattern for trunk-based development
// All code merges to main immediately, but is gated behind flags

const featureFlags = {
    newCheckoutFlow: process.env.FF_NEW_CHECKOUT === 'true',
    darkMode: process.env.FF_DARK_MODE === 'true',
    aiRecommendations: process.env.FF_AI_RECS === 'true'
};

function renderCheckout(cart) {
    if (featureFlags.newCheckoutFlow) {
        // New checkout — still in development
        return renderNewCheckout(cart);
    }
    // Existing checkout — shown to all users
    return renderLegacyCheckout(cart);
}

// Feature flags can be:
// 1. Boolean (on/off)
// 2. Percentage-based (10% of users)
// 3. User-targeted (internal employees only)
// 4. Environment-specific (staging: on, production: off)
console.log("Feature flags loaded:", featureFlags);

Now let's see how a developer's daily workflow looks in trunk-based development. The key difference from other strategies is speed: branches are created and merged within hours, not days or weeks. Reviews happen in real-time, and CI/CD deploys every merge automatically:

# ===================================================
# Trunk-Based Development: A Developer's Daily Workflow
# ===================================================
# CONTEXT: You're adding a search indexer to a SaaS
# platform. Your team deploys 10+ times per day.
# Feature flags gate all incomplete work.
# ===================================================

# Step 1: Start of day — pull the latest trunk
# In TBD, main moves FAST (many commits per day)
git checkout main
git pull origin main

# Step 2: Create a VERY short-lived branch (optional)
# Some TBD teams commit directly to main — others use
# branches that live <1 day. Either is valid.
# Prefix with your initials for quick identification
git checkout -b wz/add-search-index

# Step 3: Make focused, small changes
# Each commit should be independently safe to deploy
# The feature flag ensures users won't see unfinished work
git add src/search/indexer.ts
git commit -m "feat: add search indexer behind FF_SEARCH flag"

# Step 4: Push and create PR (reviewed within HOURS, not days)
# In TBD, reviews are the highest priority — blocking a PR
# means blocking deployment for everyone
git push origin wz/add-search-index
gh pr create --title "feat: search indexer (flagged)" --reviewer @pair-partner

# Step 5: PR is reviewed immediately (often within 30 minutes)
# This is a cultural requirement — TBD only works with fast reviews

# Step 6: After approval, merge and delete the branch
gh pr merge --squash
git checkout main
git pull origin main
git branch -d wz/add-search-index

# Step 7: CI/CD pipeline deploys to production AUTOMATICALLY
# The feature is invisible to users because FF_SEARCH=false
# in the production environment configuration.
# When the feature is complete and tested, flip the flag!

Trunk-Based Development at Scale

The world's largest engineering organisations use trunk-based development:

  • Google — 25,000+ engineers commit to a single monorepo trunk. Over 80,000 commits per day. Automated testing gates every change.
  • Meta — Engineers commit to trunk with "landcastle" (a pre-merge test system that validates changes before they land)
  • Netflix — Trunk-based with feature flags. Their system handles thousands of deployments per day across microservices
  • Spotify — Moved from GitFlow to trunk-based development as they scaled. Deployment frequency increased from weekly to many-times-daily
Research Finding DORA State of DevOps Report

Trunk-Based Development and Elite Performance

The DORA research programme (spanning 7 years and 36,000+ respondents) found that elite-performing teams are 2x more likely to practice trunk-based development than low performers. They also found that teams with branches living longer than one day had significantly lower deployment frequency and higher change failure rates.

The correlation is clear: short-lived branches (or no branches at all) correlate with higher throughput and better stability — contradicting the intuition that more branching equals more safety.

DORA metrics deployment frequency change failure rate

Workflow Comparison Matrix

Use this table to choose the right workflow for your team:

Dimension GitFlow GitHub Flow GitLab Flow Trunk-Based
Deploy frequency 2-4 weeks Daily 1-2 weeks Multiple/day
Team size 10-100+ 2-20 5-50 Any (with maturity)
Complexity High Low Medium Low (ops: High)
Release model Versioned Continuous Staged Continuous
CI requirement Moderate High High Very high
Feature flags needed No Optional Optional Essential
Multiple versions Yes No Yes No
Merge conflicts Frequent (large) Rare (small) Occasional Minimal

Monorepos vs Polyrepos

Orthogonal to branching strategy is repository structure. A monorepo contains all of an organisation's code in a single repository. A polyrepo (or multi-repo) approach uses one repository per service, library, or team.

Monorepo Advantages

  • Atomic changes — A single commit can update an API, its client library, and the documentation simultaneously
  • Code sharing — Shared libraries live alongside consuming code, eliminating versioning overhead
  • Unified CI — One pipeline, one set of tools, one configuration
  • Visibility — Every developer can see, search, and learn from all code
  • Dependency management — One version of each dependency across the entire organisation

Polyrepo Advantages

  • Team autonomy — Teams own their repo, their CI, their release cadence
  • Access control — Trivial to restrict access per repository
  • Tooling simplicity — Standard Git works without custom infrastructure
  • Smaller clone sizes — Developers only clone what they need
  • Independent deployability — Each service has its own lifecycle

Monorepo Tooling

Managing a monorepo at scale requires specialised tooling. Standard npm install or make build won't scale when you have 50+ packages — you need tools that understand which packages changed and only rebuild/retest those. Here are the major monorepo build tools:

Tool Language Key Feature Used By
Bazel Any Hermetic builds, remote caching Google, Stripe
Nx JS/TS Affected detection, computation caching Many OSS projects
Turborepo JS/TS Incremental builds, remote caching Vercel, many startups
Pants Python/Go/Java Fine-grained invalidation Toolchain
Lerna JS/TS Package publishing Babel, Jest (legacy)

Let's look at a practical monorepo setup using Turborepo (one of the most popular tools for JavaScript/TypeScript monorepos). The package.json at the repository root defines the workspace structure — telling the package manager where to find sub-packages:

// Root package.json — defines the monorepo workspace structure
// All packages live under packages/* and apps/*
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*"
  ],
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "dev": "turbo run dev --parallel"
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.4.0"
  }
}

The turbo.json configuration file tells Turborepo about task dependencies and caching. For example, "build" depends on upstream packages being built first (^build), while "lint" has no dependencies and can run in parallel across all packages:

// turbo.json — defines how tasks relate to each other
// Turborepo uses this to parallelise and cache intelligently
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": []
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Branch Protection & Policies

Regardless of which branching strategy you choose, protecting your main branch is non-negotiable for professional teams. Branch protection rules enforce quality gates before code reaches production.

Essential Protection Rules

  • Required reviews — At least 1-2 approved reviews before merge
  • Status checks — CI must pass (build, test, lint) before merge is allowed
  • Signed commits — Verify author identity with GPG keys
  • Linear history — Require rebase or squash merge (no merge commits)
  • Up-to-date branches — Branch must be current with main before merging
  • No force push — Prevent history rewriting on protected branches

You can configure branch protection rules through the GitHub web interface (Settings → Branches → Add rule), or declaratively using the probot/settings app which reads configuration from a YAML file in your repository. Here's what a complete branch protection configuration looks like:

# ===================================================
# Branch Protection Configuration
# ===================================================
# File: .github/settings.yml (using probot/settings app)
# This declaratively configures GitHub branch protection
# so your rules are version-controlled alongside your code.
# ===================================================
branches:
  - name: main
    protection:
      # Require at least 2 approving reviews
      # Stale reviews are dismissed when new commits are pushed
      required_pull_request_reviews:
        required_approving_review_count: 2
        dismiss_stale_reviews: true
        require_code_owner_reviews: true

      # These CI checks MUST pass before merge is allowed
      # 'strict: true' means the branch must be up-to-date with main
      required_status_checks:
        strict: true
        contexts:
          - "ci/build"
          - "ci/test"
          - "ci/lint"
          - "security/snyk"

      # Even admins must follow these rules (no bypassing)
      enforce_admins: true

      # Only allow squash/rebase merges (keeps history linear)
      required_linear_history: true

      # Only release managers can push directly (everyone else uses PRs)
      restrictions:
        users: []
        teams: ["release-managers"]

For teams that require signed commits (common in regulated industries and open-source projects), here's how to set up GPG commit signing. This proves cryptographically that commits actually came from who they claim to be from:

# ===================================================
# Setting Up GPG Commit Signing
# ===================================================
# WHY: Signed commits prove your identity cryptographically.
# Without signing, anyone can set their git config to your
# name/email and make commits that LOOK like they're from you.
# ===================================================

# Step 1: Generate a GPG key pair (public + private)
# Choose RSA 4096-bit for maximum security
gpg --full-generate-key

# Step 2: List your keys and find the key ID
# The key ID is the hex string after "rsa4096/"
gpg --list-secret-keys --keyid-format=long
# Example output: sec   rsa4096/ABC123DEF456 2024-01-01
#                            ^^^^^^^^^^^^^^ this is your key ID

# Step 3: Tell Git to use this key for all commits
git config --global user.signingkey ABC123DEF456
git config --global commit.gpgsign true
# Now every commit you make will be automatically signed

# Step 4: Export your PUBLIC key and add it to GitHub
# Go to: GitHub > Settings > SSH and GPG keys > New GPG key
gpg --armor --export ABC123DEF456
# Copy the entire output (including BEGIN/END lines) and paste into GitHub

# Step 5: Verify that signing works
git commit --allow-empty -m "test: verify GPG signing"
git log --show-signature -1
# You should see "Good signature from..." in the output

Case Studies

Case Study Google Engineering

Google's Monorepo and Trunk-Based Development

Google stores virtually all of its code — billions of lines across thousands of projects — in a single monorepo called "google3". Over 25,000 engineers commit to the same trunk daily, producing 80,000+ commits per day.

How it works:

  • Custom VCS called Piper (Perforce-derived) handles the scale Git cannot
  • TAP (Test Automation Platform) runs affected tests for every change
  • Changes go through mandatory code review before submission
  • Feature flags gate all incomplete work
  • Automated code health tools refactor code globally

Result: Despite enormous scale, Google ships hundreds of production changes per hour with low failure rates.

monorepo trunk-based 25k+ engineers
Case Study Atlassian

Atlassian's GitFlow to Trunk-Based Migration

Atlassian (makers of Jira and Bitbucket) initially promoted and used GitFlow extensively. As their products moved to cloud-first SaaS delivery, they found GitFlow's overhead unsustainable:

  • Merge conflicts consumed 15-20% of developer time
  • Release branches delayed features by 2-3 weeks
  • Hotfix branches created divergence nightmares

Migration: They moved to trunk-based development with feature flags over 6 months. Deployment frequency increased from bi-weekly to daily. Their documentation (atlassian.com/git) now recommends trunk-based for SaaS products.

migration SaaS delivery speed

Exercises

Exercise 1 — Strategy Selection: Your company has 40 developers building an on-premises enterprise application with quarterly releases. Customers run versions 3.x, 4.x, and 5.x simultaneously. Which branching strategy do you recommend and why? What would change if the product moved to SaaS?
Exercise 2 — Migration Plan: You've been hired to migrate a team from GitFlow to trunk-based development. The team currently has feature branches that live for 2-3 weeks. Write a phased migration plan (4-6 phases) that reduces branch lifetime incrementally without disrupting delivery.
Exercise 3 — Monorepo Evaluation: Your organisation has 12 microservices, each in its own repository. Teams spend significant time keeping shared libraries in sync. Evaluate whether migrating to a monorepo (using Nx or Turborepo) would improve or harm delivery speed. List 5 criteria for your decision.
Exercise 4 — Branch Protection Design: Design a branch protection policy for a fintech startup (10 developers, deploying 3x/day, SOC 2 compliant). Specify: required reviewers, status checks, merge strategy, and commit signing requirements. Justify each choice.

Conclusion & Next Steps

Your branching strategy is not a religious choice — it's an engineering decision driven by your deployment model, team size, compliance needs, and product lifecycle. GitFlow serves versioned software with regulatory requirements. GitHub Flow serves SaaS teams deploying continuously. Trunk-based development serves organisations that invest in engineering maturity to achieve maximum delivery speed.

The trend across the industry is clear: shorter-lived branches, faster merges, and more automation. If your branches live longer than 2 days, you're accumulating integration risk. If they live longer than a week, you're almost certainly creating unnecessary merge conflicts.

Next in the Series

In Part 12: Build Systems & Dependency Management, we'll explore how code transforms into deployable artifacts — npm, Maven, Gradle, pip, lock files, reproducible builds, and the dependency management practices that prevent "works on my machine" failures.