Categories
Software

Git Branching Strategies That Scale

I’ve worked in codebases with no branching strategy at all – everyone committing to main, hoping for the best – and I’ve worked in codebases with elaborate branching models that required a flowchart to understand. Neither extreme is good. The ideal branching strategy is the simplest one that keeps your main branch deployable and lets multiple engineers work in parallel without stepping on each other.

What that looks like in practice depends on your team size, release cadence, and whether you’re doing continuous deployment or scheduled releases. Here’s how I think about the common models and when each one earns its complexity.

Trunk-based development: the floor, not the ceiling

Trunk-based development (TBD) means everyone commits to a single branch (usually called main or trunk) frequently – ideally multiple times a day. Feature branches exist but are short-lived, measured in hours to a day or two at most. Long-lived feature branches are considered a smell.

This sounds chaotic if you haven’t tried it, but it’s the model behind how most high-performing engineering teams ship software. The discipline comes from feature flags and a test suite you trust. Incomplete features ship behind a flag that’s off in production; you merge the code before the feature is ready to show users.

# Example: simple feature flag check in application code
def render_new_dashboard(user):
    if feature_flags.is_enabled('new_dashboard', user_id=user.id):
        return render_template('dashboard_v2.html')
    return render_template('dashboard.html')

The payoff: integration problems surface immediately. You spend zero time resolving massive merge conflicts from a two-week-old branch. Your CI runs on code that reflects the actual state of the codebase rather than a parallel universe that diverged a week ago.

Where it breaks down: if your CI is slow (more than ten minutes to a green run), trunk-based development becomes painful. If you don’t have feature flags, half-finished work leaks into production. And if your team doesn’t have a culture of small commits, you end up with giant commits to main that are just as hard to review and revert as a big branch merge.

GitHub Flow: lightweight and honest

GitHub Flow is the simplest model that adds a review step. The rules are: main is always deployable; all work happens in a branch off main; branches are merged via pull request after review; merged code is deployed promptly.

git checkout main
git pull origin main
git checkout -b feature/OC-123-add-csv-export
# ... work ...
git push origin feature/OC-123-add-csv-export
# open PR, get review, merge, deploy

The branch naming convention doesn’t matter much – what matters is that branches are short-lived. I’ve seen “GitHub Flow” degenerate into branches that sit open for two weeks waiting for review, which removes most of the benefit. If PRs aren’t getting reviewed within a day, the bottleneck is process, not branching.

GitHub Flow is the right default for most teams shipping a web product continuously. It’s simple enough to explain in five minutes, integrates with every CI/CD system naturally, and doesn’t require ceremony.

Git Flow: when you need scheduled releases

Git Flow was introduced by Vincent Driessen in 2010 and became the dominant model for a while. It uses multiple long-lived branches: main (production), develop (integration), feature branches off develop, release branches that freeze features and allow only bug fixes, and hotfix branches off main for emergency patches.

This model makes sense when you have scheduled releases – say, a versioned SDK or a mobile app that goes through an app store review. The release branch gives you a stabilization period without blocking new feature work on develop.

It does not make sense for a web product that deploys multiple times a day. The overhead of maintaining two long-lived branches (main and develop), creating release branches, and merging hotfixes into both is real. I’ve seen teams cargo-cult Git Flow onto a continuous deployment web app and spend 20% of their git time on bookkeeping that adds zero value.

Branch naming and commit hygiene

Regardless of the model, a few conventions make git history actually useful:

Name branches by ticket ID and a short description: feature/OC-456-user-notifications or fix/OC-789-null-pointer-login. The ticket ID makes it easy to trace a branch to a requirement.

Write commit messages that explain the why, not the what. The diff shows what changed. “fix null pointer on login when user has no profile” is more useful than “fix bug”. The conventional commits spec (conventionalcommits.org) is worth reading – it formalizes a message structure that tooling (changelog generators, release scripts) can parse.

feat(auth): add magic link login for enterprise accounts

Adds a passwordless login flow using time-limited tokens.
Required by enterprise customers who cannot use OAuth providers.
Refs: OC-892

Squash merge vs. merge commit is a preference call. Squash gives you a clean history where each PR is one commit – good for git log --oneline readability. Merge commits preserve the full development history. I prefer squash for feature branches and merge commits for release branches.

Scaling to a larger team

Once a team grows past about fifteen engineers, a few things change. Code review becomes a bottleneck unless you have clear ownership (CODEOWNERS files help). Long-lived feature branches become more tempting because coordinating across teams on a fast-moving trunk is harder.

The answer isn’t a more complex branching strategy – it’s better tooling. Merge queues (supported natively in GitHub and GitLab) serialize competing PRs and run CI on the projected merged result before actually landing the commit. This eliminates a class of “passed CI on my branch but broke main” incidents that become more common as the team grows.

The Git documentation on branching is worth reading even if you’ve used Git for years – the mental model of what branches actually are (pointers to commits) clarifies a lot of edge cases: git-scm.com/book/en/v2.

Whatever model you pick, the most important thing is that the team understands it and applies it consistently. A mediocre branching strategy applied consistently beats a perfect one that people route around.