Choosing a branching strategy is one of those decisions that shapes how your team works every day. Get it right and deployments are predictable, code reviews are focused, and production incidents are recoverable. Get it wrong and you're spending Friday afternoons untangling merge conflicts instead of shipping.

This post covers the main branching strategies — GitLab Flow, Feature Branch Workflow, GitHub Flow, Git Flow, and Trunk-Based Development — and gives you a practical framework for choosing between them.

The branching strategy landscape

  • Git Flow — structured branching with main, develop, feature, release, and hotfix branches. Powerful for products with scheduled releases and multiple maintained versions. Complex for everything else.
  • GitHub Flow — simple: main branch plus short-lived feature branches. Merge to main, deploy immediately. Works well for continuous deployment to a single environment. Falls apart when you need staging gates.
  • Feature Branch Workflow — feature branches, main, and Git tags for deployments. No environment branches at all. Tags trigger CI/CD pipelines. More on this below.
  • GitLab Flow — adds environment branches (staging, production) to the GitHub Flow model. Code promotes through environments before reaching production. CI/CD runs at each stage.
  • Trunk-Based Development — everyone commits to main, all branches live hours not days. Requires strong CI coverage and feature flags. Used at Google, Meta, Amazon.

How GitLab Flow works

The core idea is simple: branches map to environments, and code flows in one direction — from feature branches through environment branches toward production.

The persistent branches are:

  • main — the source of truth for active development. Always deployable, but not necessarily deployed to production yet.
  • staging (or pre-production) — receives merges from main. Deployed to your staging environment. QA and integration testing happen here.
  • production — receives merges from staging. Only code that has passed staging gets here. This is what's live.

Feature branches are created from main, developed, reviewed via merge request, and merged back to main when ready. From main, code is promoted to staging, then to production. Code never flows backwards — you don't cherry-pick from production back to main. If a fix is needed, it gets made on main and promoted forward.

main
 ├── feature/user-auth         (merge to main when done)
 ├── feature/dashboard-redesign (merge to main when done)
 │
staging                        (merge from main when ready for QA)
 │
production                     (merge from staging after QA passes)

Two variants: environment branches and release branches

Environment branches — used for continuous delivery where you want code to pass through staging before production. Each branch is deployed to its corresponding environment automatically via CI/CD. This is the most common setup for web applications and APIs.

Release branches — used when you ship versioned releases (mobile apps, desktop software, libraries). When you're ready to release version 2.1, you cut a release/2.1 branch from main. Bug fixes for that release go on the release branch, and if the fix applies to main too, it gets cherry-picked or merged forward.

Why GitLab Flow fits CI/CD naturally

GitLab Flow was designed with CI/CD pipelines in mind, not retrofitted to support them. Each branch triggers its own pipeline:

  • Feature branch — runs unit tests, linting, security scanning. Fast feedback for the developer.
  • Main — runs the full test suite, builds artifacts, deploys to development environment.
  • Staging — deploys to staging environment, runs integration tests and smoke tests.
  • Production — deploys to production, runs smoke tests, triggers monitoring alerts.
stages:
  - test
  - build
  - deploy

deploy_staging:
  stage: deploy
  script:
    - kubectl apply -f k8s/staging/
  only:
    - staging

deploy_production:
  stage: deploy
  script:
    - kubectl apply -f k8s/production/
  only:
    - production
  when: manual  # require manual approval for production

Merge requests as the quality gate

In GitLab Flow, merge requests (MRs) serve multiple purposes beyond just code review:

  • Discussion thread — the entire context of a change lives in the MR: description, linked issue, pipeline status, review comments, approvals
  • CI gate — pipelines must pass before merge is allowed
  • Approval gate — require a minimum number of approvals from specific people or groups
  • Environment promotion — merging to staging or production triggers the corresponding deployment

Handling hotfixes

  1. Create a hotfix branch from production (not main)
  2. Make the fix, open an MR targeting production
  3. After production is stabilized, merge the fix forward to staging and then to main

The critical discipline is step 3. Hotfixes applied to production must flow forward to main — if they don't, the fix disappears the next time staging or production gets updated.

Feature Branch Workflow — tags instead of environment branches

There's another approach worth knowing about, simpler than GitLab Flow but more controlled than plain GitHub Flow: the Feature Branch Workflow with Git tags.

The structure is minimal — only two types of branches exist:

  • main — always production-ready, single source of truth
  • feature/* — short-lived, one per task or fix, rebased on main and merged when done

No staging branch, no production branch. Instead, deployments are triggered by Git tags. When you're ready to deploy, you tag a commit on main:

git tag v1.2.0
git push origin v1.2.0

The CI/CD pipeline fires on the tag and deploys to the target environment:

deploy_dev:
  stage: deploy
  script:
    - kubectl apply -f k8s/dev/
  only:
    - /^dev-.*/       # tags like dev-1.2.0 → auto deploy to dev

deploy_production:
  stage: deploy
  script:
    - kubectl apply -f k8s/production/
  only:
    - /^v\d+\.\d+\.\d+$/   # tags like v1.2.0
  when: manual              # semi-automatic — requires human approval

The deployment model:

  • dev environment — fully automatic. Tag dev-1.2.0 and the pipeline deploys immediately. No human interaction required. Fast feedback loop for the team.
  • production — semi-automatic. Tag v1.2.0, pipeline runs tests and builds, then waits for manual approval before the final deploy step executes. You get the speed of automation with a deliberate gate before the most critical step.

The advantages of this approach:

  • Immutable releases — a tag is a permanent, unchangeable point in history. You always know exactly what's deployed.
  • Clean history — no environment branches cluttering the repo. git log --oneline tells a readable story.
  • Easy rollback — redeploy the previous tag. No branch gymnastics, no merge reversal.
  • Simple to understand — anyone on the team can see what version is live just by looking at the latest production tag.
  • Deployment log built-in — your tag history is your deployment history. Who tagged, when, what commit.
  • Works with semantic versioning — tags like v1.2.3 map directly to release notes, changelogs, and package versions.

The rebase discipline that makes this work cleanly: before opening a merge request, always rebase your feature branch on the latest main:

git fetch origin
git rebase origin/main
# resolve any conflicts
git push --force-with-lease origin feature/my-feature

Rebase instead of merge keeps the history linear. When you look at git log on main, you see a clean sequence of commits — each feature landing in order, no merge commit noise. This makes git bisect reliable, blame meaningful, and the overall history readable as a narrative rather than a tangle of parallel lines converging.

GitLab Flow vs the alternatives

vs Feature Branch Workflow — Feature Branch + tags is simpler and cleaner for teams with strong CI coverage and semantic versioning. GitLab Flow gives you explicit staging gates and environment visibility in the branch structure itself, which is valuable for larger teams or compliance requirements.

vs GitHub Flow — GitHub Flow deploys directly from main to production. That works if you have one environment and good test coverage. It breaks down when you need a staging gate, when QA needs time to validate before release, or when deploying to production is not a casual action.

vs Git Flow — Git Flow adds develop, release, and hotfix branches on top of main. It's designed for products with explicit release cycles and multiple production versions. For most web applications doing continuous delivery, Git Flow is over-engineered.

vs Trunk-Based Development — TBD is faster but demands more discipline: strong CI, feature flags, and the confidence to commit directly to main. For teams not there yet, GitLab Flow or Feature Branch Workflow provides more isolation and review time.

When GitLab Flow makes sense

Good fit:

  • Teams that need a staging environment before production
  • Projects with QA processes or compliance requirements between development and release
  • Organizations where production deployments require explicit approval
  • Teams already using GitLab with built-in CI/CD pipelines
  • Medium-sized teams (5-30 developers) where GitHub Flow's simplicity isn't enough but Git Flow's structure is overkill

Not the best fit:

  • Very small teams or solo projects — Feature Branch Workflow is simpler
  • Large teams with mature CI/CD and feature flagging — Trunk-Based Development is faster
  • Products with complex release schedules and multiple maintained versions simultaneously — Git Flow provides more structure

My take

I'm a advocate for the Feature Branch Workflow with Git tags and I recommend it as the default starting point for most projects.

The setup is simple: you work on a feature branch, you rebase it on main before merging, and when you're ready to ship you create a tag. That's the entire workflow. No environment branches to synchronize, no merge commit noise, no stale staging branch that's three weeks behind main.

The rebase-on-main discipline is something I consider non-negotiable. It keeps the history linear and readable, makes code review easier because the diff is always against the current state of main, and eliminates the class of bugs that only appear because two features were merged from different base commits. It takes a few minutes extra per branch — it pays back in hours of debugging and review time saved.

For deployments, I prefer the tag-based model with two distinct levels of automation:

  • dev is fully automatic — tag it and it deploys. Fast feedback, no ceremony. Developers can see their changes in dev immediately after merge.
  • production is semi-automatic — the pipeline runs all the way to the final deploy step, which waits for manual confirmation. You get speed everywhere except the one moment where a human judgment call actually matters.

This model scales from a solo project to a team of ten without any structural changes. When you grow beyond that and need explicit staging gates, compliance approvals, or multi-environment promotion workflows, that's when GitLab Flow starts earning its additional complexity.

For GitLab users specifically, GitLab Flow is the natural next step — the tooling is built for it. But as a default, Feature Branch Workflow with tags and rebase is where I'd start every project.