← Blog

August 11, 2026

Branching strategy: main, pre, and dev

Why three long-lived branches

Most teams get away with main plus feature branches. Once you're operating services that other systems depend on in real time — payment processing, reconciliation jobs, anything where a bad deploy has a blast radius — you want a rehearsal environment that behaves like production before code actually reaches it. That's what pre is for.

  • main — production. Every commit here is deployable and, ideally, already running.
  • pre — pre-production / staging. Mirrors production infrastructure and data shape as closely as practical. This is where integration issues, migration edge cases, and config drift show up before customers see them.
  • dev — integration branch for work in progress. Where feature branches land first, before they're stable enough for pre.

Naming branches that come off main

Every short-lived branch should say what it is and what it touches, at a glance:

  • feature/<ticket-or-name> — new functionality. e.g. feature/ria-remittance-api
  • fix/<ticket-or-name> — bug fixes. e.g. fix/reconciliation-race-condition
  • hotfix/<ticket-or-name> — urgent fix branched directly from main to patch production, bypassing the normal dev → pre → main flow when something is actively broken.
  • chore/<ticket-or-name> — maintenance: dependency bumps, refactors with no behavior change, CI tweaks.
  • docs/<ticket-or-name> — documentation only.

Keep the branch name traceable to a ticket ID when you have an issue tracker — it turns git log into a map back to context.

The two-step PR flow

The core rule: nothing reaches main without having proven itself on pre first, except hotfixes.

  1. Branch off dev (or main, for hotfixes) using the prefixes above.
  2. Open a PR from the fix//feature/ branch into pre. This is where CI runs against a production-like environment, and where a teammate or QA actually exercises the change.
  3. Once pre is green and the change has been verified — manually or via automated smoke tests — open a second PR from that same fix//feature/ branch into main.
  4. Deploy main. Tag the release if you version deploys.

Note the second PR is from the same feature branch, not from pre itself. pre is a shared integration branch — by the time your change is verified, it may already contain other people's unrelated, still-in-flight work. Merging pre → main would drag all of that along with you. Merging your own branch into main ships exactly what you tested, nothing more.

This means pre is always a superset of what's about to ship, and main never receives untested surface area. If a change fails on pre, it never gets the chance to fail on main.

Handling hotfixes

Production incidents can't wait for the full pipeline. Branch hotfix/<name> directly off main, PR straight into main, deploy, then backport the same commit into pre and dev so the branches don't silently diverge. Skipping this step is how the same bug reappears three weeks later after the next regular release.

Keeping branches from rotting

  • Delete feature branches after merge — a stale branch is a stale mental model of the code.
  • Rebase or merge main back into dev regularly so dev doesn't drift far enough that merging becomes painful.
  • Treat pre as disposable: if it gets into a broken state that isn't a real bug, reset it from main plus whatever's mid-flight, rather than debugging a branch whose only job is to catch bugs.