Git Internals for Engineers

Branching Strategies Compared

A startup with five engineers adopts Git Flow because "it is what professionals use." Within a month, they have seven long-lived branches (main, develop, release/1.0, release/1.1, hotfix/auth, plus two feature branches), constant merge conflicts, and merge direction is unclear even to the team lead. They switch to trunk-based with short-lived feature branches. Within a week, the complexity evaporates. The "right" branching strategy is not about Git — it is about how often you release, how many people are working in parallel, and how much coordination you can tolerate.

This lesson walks through the three main strategies — Git Flow, GitHub Flow, and trunk-based — with honest trade-offs. Each works; each fits specific team situations. Learning to pick the one that matches your team's release cadence is more valuable than any Git command.


What a "Strategy" Means

A branching strategy is a convention about:

  1. Which branches are long-lived? (Exist forever: main, develop, release/*.)
  2. Which branches are short-lived? (Feature branches: days; hotfix branches: hours.)
  3. Where do features come from, and where do they merge to? (off develop, to develop? off main, to main?)
  4. How are releases cut? (Version tags on main? A release branch?)
  5. How are hotfixes applied? (Directly to main? Backported to releases?)

Git does not care which strategy you use — every strategy is just branches, merges, and tags. The convention exists so the team can coordinate without confusion.

KEY CONCEPT

There is no universally best branching strategy. There is only "the strategy that fits your team's release cadence and tolerance for complexity." Teams that release hourly benefit from trunk-based. Teams that ship software with 6-month release cycles benefit from Git Flow's long-lived branches. Copy a strategy because it matches your cadence, not because a blog post said so.


Trunk-Based Development

The simplest model: one long-lived branch (main or trunk). Everything else is short-lived and gets merged back in quickly.

main:   ─── A ─── B ─── C ─── D ─── E ─── F ─── ─→
              \   /     \   /
               f1        f2           ← feature branches live hours to days

The rules

  • One main branch. Everything merges to it.
  • Feature branches live < 1-2 days. Merge to main frequently.
  • Main is always deployable. CI must pass; never merge broken code.
  • Feature flags gate incomplete features in main, not branches.
  • Releases are tags on main. v1.0.0, v1.1.0 — not separate branches.
  • Hotfixes are commits on main (possibly cherry-picked to release tags if needed).

Why teams adopt it

  • Integration is continuous. No month-long feature branches drifting from main.
  • Conflict is rare. Short branches have little to conflict with.
  • CI is always green. Merge gate + short cycles = rapid detection of issues.
  • Deploy is always possible. Main is always shippable.

Typical workflow

git checkout -b feature/quick-thing main
# ... work for a few hours ...
git push origin feature/quick-thing
# Open PR, review, merge (squash or rebase)
git checkout main
git pull
git branch -d feature/quick-thing     # local cleanup

Multiple times a day. The rhythm is "branch, work briefly, PR, merge, repeat."

Feature flags for incomplete work

Because main must always be deployable, half-finished features cannot just live on main commented out. The pattern:

# Feature flag gates the new code
if feature_flag('new-checkout-flow'):
    use_new_checkout()
else:
    use_old_checkout()

Flags let you:

  • Merge incomplete work to main safely (the flag is off).
  • Roll out gradually (10% of users, then 50%, then 100%).
  • Roll back by flipping a flag, not by reverting a commit.

Services like LaunchDarkly, Flagsmith, or homebuilt flag tables make this practical at scale.

When trunk-based is right

  • Continuous deployment. Multiple deploys per day.
  • Small to medium teams. Up to ~100 engineers with good CI.
  • Modern stack with robust feature-flag tooling and automated testing.
  • Team with CI discipline. Main breakages are rare.

When trunk-based struggles

  • Long release cycles. If you release twice a year, there is little gain from merging hourly.
  • Teams without good CI. Trunk-based requires main to stay green; needs strong automation.
  • Embedded / enterprise software. Where "install at a customer, upgrade quarterly" is the release model.

GitHub Flow

A pragmatic simplification of trunk-based, with slightly more ceremony around PRs.

main:   ─── A ─── B ──────────── E ─── F ─── ─→
              \                /
               C ─── D─────PR─/      ← feature branches via PRs

The rules

  • One main branch.
  • Every change goes through a pull request.
  • Feature branches exist for the duration of a PR — usually days, sometimes weeks.
  • Main is always deployable.
  • Deploy is driven from main (or cuts from main at review checkpoints).
  • No long-lived release branches by default (though hotfix branches may exist briefly).

The workflow

git checkout -b feature/add-auth main
# ... work ...
git push origin feature/add-auth
# Open PR on GitHub
# Review / CI / approval
# Merge via GitHub (squash, merge, or rebase)
git checkout main && git pull

How it differs from trunk-based

Mostly: it is more permissive about longer-lived feature branches (a PR might take a week of back-and-forth), but the main branch is still the single source of truth.

When GitHub Flow fits

  • Smaller teams (< 50 people).
  • Web / SaaS products with frequent deploys.
  • Teams comfortable with PRs as the formal integration process.
  • When "everyone on main is always deployable" is a priority.

Git Flow (Classic)

The most complex: multiple long-lived branches with prescribed roles.

main:     ─── (tag v1.0) ──────── (tag v1.1) ──── →   stable releases only
                 ↑                    ↑
develop:  ─── A ─── B ─── C ─── D ─── E ─── F ─── → integration
              \         /           \         /
              feature/x             feature/y      ← features
                          \
                          release/1.1 → (bug fixes; eventually merged to main + develop)

                          (hotfix/*   → emergency fixes from main; merged to main + develop)

The branches

  • main — only release-quality commits; tagged per release.
  • develop — integration branch where features land and bake.
  • feature/* — individual features, branched from and merged to develop.
  • release/* — short-lived, forked from develop to stabilize for a release. Bug fixes go here; merged to main (becoming the release) and back to develop.
  • hotfix/* — emergency patches branched from main; merged to main and develop.

The complexity

  • Features branch from develop, merge to develop.
  • Releases branch from develop, merge to main AND develop.
  • Hotfixes branch from main, merge to main AND develop.
  • Every merge is a two-way sync operation.

Tools like git-flow (the gitflow-avh extension) automate the commands, but the cognitive overhead remains.

When Git Flow fits

  • Scheduled releases (every 2 weeks, monthly, quarterly).
  • Parallel maintenance of old versions. You support v1.0 while developing v2.0.
  • Formal release management. QA sign-off, change control, release documentation.
  • Teams with dedicated release engineering. Someone owns the release branch.

Example: enterprise B2B software that releases a major version quarterly with hotfix support for the previous three versions.

When Git Flow hurts

  • Continuous deployment. The release branch adds delay with no benefit.
  • Small teams. The overhead is not justified.
  • Fast-moving products. Develop diverges too far from main too fast.
WARNING

Git Flow gets blamed for complexity, but most of the complexity comes from misapplying it. Git Flow was designed for products with explicit release cycles. Using it for a web app that deploys 10 times a day creates overhead without value. The creator of Git Flow has publicly said it is inappropriate for modern web development — use GitHub Flow or trunk-based instead.


GitLab Flow (A Middle Ground)

A hybrid combining trunk-based simplicity with environment/release branches for ops.

main:        ─── A ─── B ─── C ─── ─→
                     |       |
pre-prod:            A       C   ← promoted from main on cadence
                     |       |
production:          A       C   ← promoted from pre-prod after validation

The idea

  • main is where work merges (like trunk-based).
  • Environment branches (pre-production, production) are deployment gates — commits are promoted from main to pre-prod to production.
  • Release branches (release/1.0) for actual versions shipped to customers.

The flow

# Normal work: merge to main
git checkout -b feature/add-x main
# PR to main, merge

# Deploy: merge main to pre-production
git checkout pre-production
git merge main    # or rebase; team choice

# Production deploy: merge pre-production to production
git checkout production
git merge pre-production

# For customer releases (if applicable):
git checkout -b release/1.5 main

When GitLab Flow fits

  • You need visible deployment gates (via branches) without full Git Flow's complexity.
  • You have non-trivial staging / pre-prod / prod pipelines.
  • You release versioned products but deploy continuously internally.

Comparing the Options

DimensionTrunk-BasedGitHub FlowGit FlowGitLab Flow
Long-lived branches1 (main)1 (main)2 (main, develop) + release branches2-3 (main + env branches) + release branches
Feature branch lifetimeHours-daysDays-weeksWeeksDays-weeks
Release cadence supportedHourlyDailyWeekly-quarterlyDaily-weekly
Hotfix patternCommit on main + flag togglePR to main + deployhotfix/* branch merged to main + developCommit on main, cherry-pick to release/env
Release representationTagTagrelease/* branch + tag on mainrelease/* branch + tag
ComplexityLowLow-mediumHighMedium
Fits team sizeAnySmall-mediumMedium-largeMedium-large
Fits release typeContinuousContinuousVersioned / batchedContinuous with staging gates
Feature flags expectedYesSometimesRarelyYes

Picking a Strategy

Ask these questions:

1. How often do you deploy?

  • Multiple times a day → trunk-based or GitHub Flow.
  • A few times a week → GitHub Flow or GitLab Flow.
  • Every 2 weeks → GitHub Flow with release tags, or Git Flow.
  • Monthly/quarterly → Git Flow.
  • To customers with upgrade cycles → Git Flow.

2. Do you support multiple versions in parallel?

  • No (only HEAD ships) → trunk-based / GitHub Flow.
  • Yes (v1.0 still supported while v2 ships) → Git Flow (release branches give you this).

3. Do you have robust CI and feature flags?

  • Yes → trunk-based is feasible.
  • Not really → GitHub Flow with good PR review.

4. How big is the team?

  • < 10 → any strategy works; trunk-based is simplest.
  • 10-100 → GitHub Flow or trunk-based with merge queue.
  • 100+ → trunk-based with strong tooling (monorepo + merge queue + build graph), or GitLab Flow with release discipline.

5. Who merges PRs, and how?

  • Everyone merges their own — less ceremony; trunk-based / GitHub Flow.
  • Release manager controls main — Git Flow / GitLab Flow.
PRO TIP

When picking a strategy, err toward simpler. It is much easier to add complexity later when you genuinely need it (first time you have to back-port a hotfix to three old releases) than to unwind complexity that was adopted prematurely. Most teams are happier on trunk-based or GitHub Flow than they think, even if they have read blogs suggesting Git Flow.


Implementing the Strategy

Whatever you pick, enforce it via platform settings:

Branch protection (GitHub example)

# Via GitHub settings UI, or as code via API
# For `main`:
- Require pull request reviews before merging: 1 reviewer
- Require status checks to pass: ci, linting, tests
- Require branches to be up to date before merging
- Require linear history (for trunk-based / rebase-merge teams)
- Include administrators
- Do not allow force pushes
- Do not allow deletions

Merge options

Decide what the "Merge PR" button does:

  • Create a merge commit (preserves branch history as a merge commit).
  • Squash and merge (one commit per PR on main).
  • Rebase and merge (linear history, preserves individual commits).

Teams standardize on one. Inconsistent use causes confusion.

CODEOWNERS

# .github/CODEOWNERS
# Paths → required reviewers
services/payments/**    @payments-team
infra/**                @platform-team @sre-team
*.md                    @docs-team

Auto-requests reviews from the right people for every PR. Essential at team scale.

Merge queues

For busy main branches:

  • GitHub Merge Queue.
  • Mergify.
  • Bors-ng.
  • GitLab Merge Trains.

A merge queue serializes PR merges with pre-merge CI runs, preventing two green PRs from merging into a broken combined state.


Migrating Between Strategies

Git Flow → Trunk-Based

The typical "simplification" migration.

  1. Stop creating new develop branches. All new work merges to main.
  2. Merge the current develop branch into main.
  3. Convert in-flight features to branch off main.
  4. Delete release branches after their content is tagged on main.
  5. Adopt feature flags for work-in-progress.
  6. Shorten feature branch lifetimes.

Allow a few months of transition. Not all features need to switch immediately.

Trunk-Based → Git Flow

Rarer, but happens when a product starts serving multiple customer versions simultaneously.

  1. Create a develop branch from current main.
  2. New features branch from develop and merge back.
  3. Cut release/X.Y branches from develop for each release.
  4. Main contains only tagged releases.
  5. Establish release management processes (QA sign-off, change control).

Common Conventions Within Strategies

Regardless of the strategy, some conventions are near-universal:

  • main (not master). Modern default.
  • develop or development if using Git Flow.
  • feature/*, fix/*, hotfix/*, release/* naming.
  • Tags for releases: v1.0.0, v1.0.1.
  • Semver for version numbers: major.minor.patch.
  • Conventional Commits for messages: feat(scope): subject.

Key Concepts Summary

  • Trunk-based: one main branch; short-lived feature branches; continuous integration; feature flags for incomplete work. Best for continuous deployment.
  • GitHub Flow: one main branch; all changes via PRs; simpler than trunk-based for small teams.
  • Git Flow: main + develop + release/* + hotfix/*; heavy process for scheduled releases with parallel version support.
  • GitLab Flow: main + environment branches (pre-prod, prod) + release/*. Good for staging-heavy pipelines.
  • Pick based on release cadence, team size, and complexity tolerance. Not based on what is trendy.
  • Feature flags enable trunk-based. Without them, you need branches for incomplete work.
  • Enforce the strategy in platform settings: branch protection, required reviewers, merge options, CODEOWNERS.
  • Merge queues prevent race conditions on busy branches.
  • Err simpler. Add complexity when a real need surfaces, not preemptively.

Common Mistakes

  • Adopting Git Flow on a small team doing continuous deployment. You get all the overhead and none of the benefit.
  • Long-lived feature branches in any strategy. They diverge from main, accumulate conflicts, and rot.
  • Skipping feature flags on trunk-based. Incomplete work gets stuck in branches, defeating the point.
  • Inconsistent merge options per PR. Sometimes squash, sometimes merge commit — main becomes unreadable.
  • No branch protection on main. Force-pushes and unreviewed direct commits slip through.
  • Release managers "protecting" main by manually reviewing all commits — becomes a bottleneck. Automate via CI and merge queues instead.
  • Keeping develop around after switching to trunk-based. Dead branch causing confusion.
  • Using Git Flow for a product that has exactly one version in production. All that branch ceremony for nothing.
  • Letting main break on trunk-based without an incident response. Main must always be deployable; a red main is a team emergency.
  • Assuming branching strategy fixes team issues. If your team has poor communication, no strategy will save them; if they communicate well, most strategies work.

KNOWLEDGE CHECK

A team of 12 engineers working on a SaaS product deploys to production 5-10 times a day. Their current Git Flow process (main, develop, release/*, hotfix/*) causes friction: merges are one-way one day, two-way the next; conflict in develop is constant; releases are branch gymnastics. Everyone agrees it needs simplification. What should they switch to, and what is the one-week migration plan?