false
Feature Flags

Best Practices for Feature Branching

A graphic of a bar chart with an arrow pointing upward.

Most teams treat feature branching as a given — the default workflow, the safe choice, the thing everyone does.

But "everyone does it" is not the same as "it's the right tool for the job," and the gap between those two things is where merge conflicts, delayed feedback, and bloated branch histories quietly accumulate.

This article is for engineers, PMs, and data teams who use feature branches every day and want to understand when that's the right call — and when it isn't.

It makes a specific argument: feature branching is overused, and for most release control problems, trunk-based development paired with feature flags is a cleaner solution. Here's what the article covers:

  • Why feature branching became the industry default and what problems it was actually designed to solve
  • The hidden costs of long-lived branches — merge conflicts, CI/CD incompatibility, and the cognitive overhead nobody budgets for
  • How trunk-based development works in practice and why it's not as reckless as it sounds
  • How feature flags decouple deployment from release, replacing the job branches were never well-suited for
  • Concrete feature branching best practices for teams that aren't ready to abandon branches entirely

The article moves in that order — starting with a fair account of why feature branching earned its place, then building the case for where it falls short, then giving you a practical framework for using branches well when you do use them.

If you're here for the concrete practices, they're in Section 5 — but the sections before it explain why those practices are framed the way they are.

What feature branching is—and why it became the default

Before questioning whether feature branching is overused, it's worth understanding why so many teams adopted it in the first place.

The workflow didn't become dominant by accident — it solved real, painful coordination problems that teams ran into as distributed version control became the norm. If you're going to challenge a practice that most engineering organizations treat as table stakes, you need to start by taking its benefits seriously.

Feature branching is a convention, not a technical constraint

Feature branching is a workflow convention built on a straightforward idea: every new feature or bug fix gets its own dedicated Git branch, isolated from the main branch until the work is complete and reviewed.

Atlassian's Git tutorial puts it plainly — "the core idea behind the Feature Branch Workflow is that all feature development should take place in a dedicated branch instead of the main branch."

One thing worth clarifying upfront: this is a convention, not a technical constraint. As Atlassian notes, "Git makes no technical distinction between the main branch and feature branches."

The isolation is enforced by team agreement, not by the version control system itself. In practice, most teams work with two branch types: feature branches for new functionality and bug branches for fixes, both originating from main.

The pull request cycle: branch, review, merge, retire

The mechanics are familiar to most engineers. A developer branches off main, builds the feature in isolation, and pushes the branch to the central repository — sharing progress with teammates without touching official code.

When the work is ready, they open a pull request, collect review and approval, then merge back to main. The branch is then retired, and the cycle repeats.

What's easy to overlook is that this model was designed with collaboration in mind from the start. Branches can be pushed to the remote repo mid-development, enabling teammates to review work in progress, not just finished code.

Pull requests, as Atlassian describes them, "give other developers the opportunity to sign off on a feature before it gets integrated into the official project" — and can also be opened specifically to solicit early feedback and discussion.

The three problems it was designed to solve

Feature branching earned its place by addressing three concrete problems that teams faced when committing directly to a shared codebase.

The first is parallel development without interference. When multiple developers work on the same codebase simultaneously, uncoordinated commits to a shared branch create conflicts and instability.

Feature branches give each developer an isolated workspace, allowing parallel work to proceed without one engineer's half-finished code disrupting another's.

The second problem is main branch stability. LaunchDarkly's guidance on feature branching describes the goal directly: after a PR is approved and merged, "the main branch will always be healthy and up-to-date with high-quality code."

Atlassian frames this as "a huge advantage for continuous integration environments" — a promise that the shared integration point is always in a deployable state.

Structured code review is the third. Pull requests, which feature branching enables naturally, create a formal checkpoint before code reaches main.

Microsoft's Azure DevOps branching guidance — which describes the workflow Microsoft uses internally — treats PR-based review as a non-negotiable part of the model, not an optional enhancement.

Why it became the industry default

The adoption story is partly about the workflow's genuine merits and partly about timing. As Git displaced centralized version control systems, teams needed workflow conventions to make sense of a tool that was deliberately flexible and unopinionated.

Feature branching filled that vacuum with clear, authoritative guidance at exactly the right moment.

Microsoft's public documentation gave the workflow institutional legitimacy — their guidance explicitly states that "even small fixes and changes should have their own feature branch," a prescription that signals how thoroughly the practice was internalized at scale.

Atlassian's tutorials made it the de facto starting point for teams learning Git. The observation that "most developers love feature branching because it makes the development process more flexible" reflects not just a technical preference but a cultural attachment that built up over years of consistent endorsement from trusted sources.

A thread on Hacker News discussing Gitflow — one of the more elaborate branching models built on top of feature branching — offers a useful meta-observation: the model spread partly because "a lot of people felt adrift when it comes to Git" and needed any clear framework.

Widespread adoption, in other words, was driven as much by the need for guidance as by the workflow's inherent superiority. That distinction matters when evaluating whether feature branching is still the best tool for the problems it was designed to solve.

The hidden costs of feature branching: merge hell, long-lived branches, and delayed integration

Feature branching feels like risk management. You isolate your work, keep main clean, and merge when you're ready.

The problem is that "when you're ready" has a way of arriving much later than planned — and by then, the codebase has moved on without you.

The core issue isn't that teams misuse feature branches. It's that the workflow is structurally designed to defer integration, and deferred integration means deferred feedback.

That deferral has a compounding cost that most teams absorb quietly, chalking it up to routine friction rather than recognizing it as a systemic signal.

How short-lived branches become long-lived ones

No team sets out to maintain a branch for three weeks. It starts as a focused, scoped piece of work — a few days, maybe a week. Then a PR sits in review. Then a dependency on another branch blocks progress. Then scope creeps. Then the holidays hit.

The workflow has no structural forcing function to prevent this drift. CloudBees describes the experience as building a room in a house, finishing the work, and then discovering the door is blocked by a wall someone else built while you weren't looking.

The problem isn't visible during development — it surfaces only at integration time, when the cost of fixing it is highest.

Merge conflicts and the compounding cost of divergence

CloudBees is direct about this: "Merge conflicts are the biggest pitfall of using feature branches. Nothing hurts more than spending unnecessary time fixing merge conflicts, especially when a feature branch has been there for a while."

The time cost is only part of the problem. The risk of accidentally removing existing code or introducing new bugs during conflict resolution increases considerably the longer the branch has lived.

In severe cases, teams end up freezing all active development to stabilize the merge — a team-wide halt caused by one branch's deferred integration.

There's a downstream debugging cost too. Complex branching histories make git bisect harder to use effectively, which slows down the identification of regressions and security issues precisely when speed matters most.

Feature branching and CI/CD: a structural incompatibility

"If you're merging every few days, you're not doing Continuous Integration, you're delaying pain."

Dave Farley, a prominent continuous delivery advocate, puts the incompatibility plainly with that quote.

This isn't a matter of degree — it's definitional. Continuous integration means integrating continuously, not integrating at the end of a feature cycle.

Branch-based workflows create feedback loops measured in days or weeks rather than hours. By the time a broken assumption surfaces, the code that introduced it is buried under layers of subsequent work.

The cognitive overhead nobody accounts for

The time lost to merge conflicts is visible. The time lost to branch management overhead is harder to measure but just as real.

A practitioner on Hacker News, reflecting on a decade of Gitflow, put it this way: "I would love all the hours back I spent in discussions around branching strategy, trying to keep complex models understood across the team (and myself), dealing with painful merges and flowing changes through, trying to figure out if a change is in a branch already."

That's not a complaint about a bad implementation of feature branching. That's a complaint about the model itself — the inherent overhead of tracking which changes live where, coordinating across branches, and maintaining shared understanding of a branching topology that changes constantly.

Isolation feels like safety—it isn't

The synthesis of all these costs points to a single structural flaw: feature branching doesn't eliminate integration risk, it defers it.

Farley's framing is worth quoting directly: "The longer a branch lives, the more your codebase diverges from reality. You're not integrating, you're isolating. And isolation KILLS FEEDBACK."

That divergence is the hidden debt. Every day a branch lives, the gap between what the branch assumes about the codebase and what the codebase actually looks like grows a little wider.

The eventual merge doesn't just cost time — it costs the feedback that continuous integration would have delivered incrementally, when it was still cheap to act on.

It's worth acknowledging that not every practitioner agrees this is a fatal flaw. Some argue it's a trade-off decision that depends on team size, test coverage, and deployment cadence. That's a fair framing.

But it's also a framing that tends to underestimate how systematically the costs compound — and how rarely teams accurately account for them when they choose the workflow.

Trunk-based development: the case for committing frequently to main

Let's be direct about the heading: "committing directly to main" is a slight provocation. Trunk-based development doesn't actually require that every developer push every commit straight to the main branch without review.

What it does require is the elimination of long-lived branches — and that distinction matters enormously for how you evaluate it. If you dismiss TBD because it sounds reckless, you're probably reacting to a caricature.

TBD is not 'no branches'—it's no long-lived branches

Atlassian defines TBD as "a version control management practice where developers merge small, frequent updates to a core 'trunk' or main branch." Harness describes it as a model where developers "frequently integrate code changes into this central codebase, often multiple times per day."

The shared principle is a single source of truth that everyone integrates against continuously — not a collection of diverging branches that get reconciled every few weeks.

Critically, TBD comes in two variants. For small teams (roughly fifteen people or fewer), committing directly to main is practical and common. For larger teams, short-lived feature branches are fully compatible with TBD — provided they stay short.

The authoritative reference site trunkbaseddevelopment.com puts a hard ceiling on this: a branch should last no more than two days. Any longer, and you're sliding back into the long-lived branch pattern that TBD is specifically designed to avoid.

These short-lived branches should also stay narrow — one or two developers at most, not a shared workspace for an entire feature team.

The two-day ceiling: why branch lifespan is the key variable

The operational rhythm of TBD is straightforward: developers commit frequently, branches are measured in hours rather than weeks, and CI validation runs before anything merges to main.

The two-day limit isn't arbitrary — it's the point at which a branch starts accumulating enough divergence from main that integration becomes genuinely painful rather than trivially easy.

Team size shapes which variant makes sense. Direct-to-trunk works well for smaller, high-trust teams where the overhead of a PR review on every small change slows things down more than it protects anything.

Larger teams benefit from the short-lived branch variant, which preserves peer review without abandoning the continuous integration discipline that makes TBD effective. The key variable isn't whether you use branches at all — it's whether those branches outlive their usefulness.

The evidence that TBD isn't a fringe idea

Atlassian makes a strong claim worth sitting with: "trunk-based development is a required practice of CI/CD." Not a recommended practice. Not a compatible practice. Required.

If your team is running feature branches that live for two or three weeks, you are, by definition, not doing continuous integration — you're doing periodic integration with a CI label on it.

Atlassian also credits TBD with increasing "software delivery and organizational performance" and describes it as "a common practice among DevOps teams and part of the DevOps lifecycle."

Harness traces its lineage to large-scale engineering organizations, noting that it has grown "from a niche strategy to a favored industry approach" refined by both small startups and large tech companies. Google's monorepo practices are frequently cited as an early, large-scale implementation of the same underlying principle.

This is not a radical workflow. It's what high-performing engineering teams have been doing for years, and the industry has largely caught up to validating it.

The obvious objection: feature flags are the answer to incomplete code in production

The pushback you're probably forming right now is legitimate: if everyone integrates to main continuously, how do you keep half-built features out of production?

This is the right question, and it's the reason many teams reach for long-lived branches in the first place — they want a structural guarantee that incomplete work stays isolated.

But branches are a blunt instrument for that job. They isolate code, not behavior. The cleaner answer is feature flags — the next section covers how they work, but the short version is that they let you merge to main while controlling who sees what at runtime.

That's the actual problem you were trying to solve with the long-lived branch.

Feature flags as a better mechanism: decoupling deployment from release

Feature branching became the dominant release control mechanism because teams needed a way to keep incomplete code away from users. That's a legitimate problem.

But it's worth asking whether a branch is actually the right tool for solving it — or whether it's just the most familiar one.

The deploy/release conflation feature branching exploits

Historically, deploy and release were synonymous — code that was deployed was immediately released. Feature branching works within that assumption.

If deployment equals release, then keeping code off the main branch is the only way to keep it away from users.

Feature flags break that assumption entirely. At their most basic, flags are runtime conditionals — if-else statements that determine which code path executes based on rules evaluated when the code runs, not when it's compiled or deployed.

You deploy the code to production. The flag controls whether any given user ever sees it. Deployment and release become two separate decisions, made at different times, by different people, for different reasons.

A practitioner on Hacker News described introducing flags to their team precisely this way: as "a means to separate deployment from launch of new features." That's the core value proposition, and it directly addresses the problem feature branching was solving — just without the merge complexity.

How flags replace branching for release control

When you use a feature branch to protect a half-finished feature, you're using version control as a release gate. The branch controls code isolation at the repository level, and you merge when you're ready to release.

The problem, as the previous sections established, is that "when you're ready to release" often turns into weeks, and by then the branch has diverged from main in ways that are painful to reconcile.

With feature flags, code merges to main continuously. The flag controls user exposure at runtime. Statsig captures the contrast well: feature flags "integrate new code into the main branch but keep it hidden until you're ready to flip the switch," which "aligns with continuous integration" in a way that long-lived branches fundamentally don't.

The practical implication is significant. A branch is binary — it's either merged or it isn't. A flag is granular. You can expose a feature to 5% of users, watch your error rates, expand to 25%, then 50%, then 100%.

You can target internal employees first, then beta users, then everyone. You can roll back instantly — not with a revert commit and a deploy, but by flipping a toggle that takes effect immediately for every user currently running your app — no deployment required.

The operational capabilities branching cannot provide

This is where the comparison stops being close. A feature branch cannot do a canary launch. It cannot run an A/B test. It cannot give you a kill switch that fires in under a minute when something goes wrong in production.

It cannot target a feature to users in a specific geography, on a specific subscription tier, or running a specific version of your mobile app.

Feature flags can do all of these things because they evaluate at runtime against user context. Platforms like GrowthBook implement this through attribute-based targeting rules — geography, device type, company ID, custom attributes — combined with percentage rollouts that use deterministic hashing so the same user always gets the same variant.

Because experimentation is built into the same platform, those rollouts can generate statistically valid experiment results directly — you're not just controlling exposure, you're measuring impact without switching tools.

Some platforms extend this further by automatically monitoring whether a rollout is degrading key signals — error rates, latency, conversion rates — and surfacing warnings before a problem becomes an incident.

The flag becomes not just a release gate but an active safety mechanism.

When a branch is still the right tool

Flags are the right mechanism for release control. Branches are still appropriate for code isolation during active development — particularly for large features involving multiple engineers, or for open-source contributions that require PR review before any code touches main.

The distinction matters: use a branch to manage who can see and modify the code while it's being written; use a flag to manage who experiences the feature once it's deployed.

One real cost worth acknowledging: flags accumulate technical debt if they're never cleaned up. The practitioner community is consistent on this — flags should be short-lived, and removing the flag evaluation code after a full rollout should be a mandatory process step, not an afterthought.

Tools like GrowthBook include stale feature detection to surface flags that have outlived their usefulness, but the discipline still has to exist at the team level. A flag left in place indefinitely is its own kind of long-lived branch problem.

Feature branching best practices: when branches are still justified and why lifespan is the key constraint

After making the case that feature branching is overused and that trunk-based development paired with feature flags is often the better path, it's worth being honest about something: most teams aren't abandoning branches tomorrow.

Branching isn't inherently wrong — it's misused. The goal here is to give you a concrete framework for using branches well, so that when you do branch, you're doing it for the right reasons and not creating the integration problems the previous sections described.

Name branches like they'll outlive the sprint

Both Atlassian and Microsoft treat descriptive naming as a non-negotiable. Atlassian's examples are instructive in their specificity: animated-menu-items and issue-#1061 — names that communicate purpose without requiring anyone to open the code.

Microsoft recommends encoding work item numbers, developer context, or feature descriptions directly in the branch name.

The practical reason this matters: a branch name is the first piece of documentation anyone sees when reviewing a pull request or scanning repository history. If your branch is named johns-work or fix-2, you've already made the reviewer's job harder.

A branch name should answer the question "what does this do?" before anyone clicks on it.

Treat branch lifespan as a signal, not just a metric

The practitioner consensus is clear: branches should live for days, not weeks. When a branch extends well beyond that window, it's usually a signal that one of two things has gone wrong: the scope of work is too large and should be broken into smaller units, or the branch is being held open to control a release — which is the wrong job for a branch.

The Hacker News practitioner consensus on this is blunt: "Use short lived branches, and merge to master. Need to do a release? Master is your release."

The principle is that a branch should live exactly as long as it takes to complete a focused, reviewable unit of work — and not a day longer.

CI validation is the technical gate, not just a nice-to-have

Microsoft, Atlassian, and LaunchDarkly all frame the pull request as the required gate before any branch merges to main. LaunchDarkly's formulation is direct: validate the code using a pull request, merge only after approval.

Microsoft frames this as the mechanism for keeping main "high quality and up-to-date." Atlassian notes that PRs give team members the opportunity to sign off before integration happens.

The implication for CI pipelines is straightforward: automated tests should run on every branch before a PR can be approved. This is the technical enforcement of the healthy main branch principle — not a policy you rely on developers to remember, but a gate the system enforces.

Branches own the code; flags own the release

This is the conceptual linchpin. Branches should control code isolation — keeping work-in-progress out of main until it's ready to integrate. Feature flags should control release timing — keeping integrated code hidden from users until it's ready to ship.

When a team keeps a branch open because a feature isn't ready for users yet, they're using the wrong tool. That's a release control problem, and branches are a code isolation tool.

The branch lifecycle ends at merge; what happens after merge — who sees the feature, when, under what conditions — is a separate concern entirely.

This is where a feature management platform earns its place in the workflow. Tools like GrowthBook let you merge code to main immediately while keeping the feature invisible to users until you're ready, with support for gradual rollouts, kill switches, and time-based activation.

The "deploy now, release later" model is only possible when you stop using branches as release gates.

Two jobs, two tools: separating code isolation from release control

The argument this article has been building comes down to a single distinction: branches and feature flags are not competing tools for the same job. They are complementary tools for two different jobs that most teams have been conflating.

Consider a concrete scenario. A team is migrating a payment flow to a new provider. The old approach: open a feature branch, build the new flow in isolation for three weeks, then attempt a merge that touches dozens of files and conflicts with two other branches that landed while the work was in progress.

The new approach: merge incremental changes to main continuously behind a feature flag, expose the new flow to 5% of users first, monitor error rates and conversion, expand the rollout gradually, and roll back in seconds if anything degrades — no revert commit, no emergency deploy.

The branch in the old approach was doing two jobs simultaneously: isolating code during development and controlling when users saw the new experience.

The flag in the new approach separates those jobs cleanly. The branch (if used at all) lives for a day or two and handles only code isolation. The flag handles release timing, targeting, and rollback — capabilities a branch was never designed to provide.

Branches defer integration. Flags defer release. Those are different problems, and conflating them is the root cause of most of the friction this article has described.

When feature branching is the right tool (and when it isn't)

Feature branching remains the right tool in specific, bounded circumstances:

  • Large features involving multiple engineers where code isolation during active development is genuinely necessary
  • Open-source contributions where PR review before any code touches main is a hard requirement
  • Experimental work that may never ship and shouldn't pollute the main branch history
  • Short-lived branches (measured in hours or a day or two) that integrate continuously and don't accumulate divergence

Feature branching is the wrong tool when:

  • The branch is open primarily because a feature isn't ready for users yet — that's a flag's job
  • The branch has lived longer than a week and is accumulating merge debt
  • The team is using the branch as a substitute for a proper release process
  • Multiple engineers are sharing a single long-lived branch as a staging area

Starting the shift: one flag where there used to be a long-lived branch

The transition toward trunk-based development and feature flags doesn't require a team-wide rewrite of process overnight. The most effective starting point is identifying one active long-lived branch and asking a single question: is this branch open because the code isn't ready to integrate, or because the feature isn't ready for users?

If the answer is the latter, that branch is your first flag candidate.

What to do next, based on where your team is:

  • If your team uses long-lived branches (more than one week): Audit one active branch. Ask whether it's open for code isolation reasons or release timing reasons. If it's the latter, that's your first flag candidate. Merge the code to main behind a flag, turn the flag off for all users, and retire the branch. Measure how the integration experience differs.
  • If your team wants to move toward TBD: Pick one feature in the current sprint. Merge it to main behind a flag instead of holding a branch open. Keep the flag off in production until the feature is ready. Observe the difference in merge friction, CI feedback speed, and the time between writing code and getting it into the shared integration point.
  • If your team already uses feature flags but still maintains long-lived branches: The branches are probably doing double duty. The decision rule above applies directly — separate the two jobs and let each tool do one thing well. Branches for code isolation during active development. Flags for release timing, targeting, gradual rollouts, and kill switches.

The goal isn't to eliminate branches. It's to stop using them as a substitute for release control — and to stop paying the integration tax that long-lived branches impose on every engineer who has to merge against a codebase that moved on without them.

Feature branching best practices, at their core, are about keeping branches short, purposeful, and scoped to code isolation. Everything else — who sees the feature, when, under what conditions, with what rollback plan — belongs to the flag.

Table of Contents

Related Articles

See all articles
Experiments
AI
What I Learned from Khan Academy About A/B Testing AI
Experiments
Designing A/B Testing Experiments for Long-Term Growth
Experiments
AI
How a Team of 4 Used A/B Testing to Help Fyxer Grow from $1M to $35M ARR in 1 Year

Ready to ship faster?

No credit card required. Start with feature flags, experimentation, and product analytics—free.