Oct 1, 2026 · devops · 38 min read · 7571 words intermediate

Branching Strategies at Big Tech, MNCs and Startups: What Each One Actually Does.

devops git ci-cd engineering-culture

Summary: There is no single "correct" branching strategy. Google, Meta and Netflix run trunk based development on a single branch with heavy automated testing and feature flags. Startups and Shopify use GitHub Flow: one main branch, short feature branches, deploy on merge. Large enterprises with versioned, shipped software often still use GitFlow or Microsoft's Release Flow, which both keep long lived branches around a release. The Linux kernel uses a fully distributed model with no shared branch at all. Pick based on release cadence, team size and how much test automation you actually have, not on what is trendy.

5strategies compared in this article
1branch in trunk based development, no exceptions
2010year GitFlow was first published by Vincent Driessen
5 to 10commits per batch in Shopify's merge queue

1. At a glance

"Branching strategy" really answers three practical questions: how long does a piece of in progress code live apart from everyone else's, how does it get back together with the rest of the codebase, and how does a specific version reach production. The five strategies below answer those three questions very differently, and the answer that fits a 2,000 person engineering org is rarely the answer that fits a 5 person startup.

StrategyUsed byMain branchesBranch lifetimeBest for
Trunk based developmentGoogle, Meta, Netflix, most SaaS at scale1 (trunk / main)HoursStrong CI, feature flags, frequent releases
GitHub FlowGitHub, Shopify, most startups1 (main)DaysWeb apps with continuous deploy
GitFlowEnterprise software, SDKs, regulated industries2 (main, develop)Weeks to monthsVersioned releases, multiple supported versions
GitLab FlowTeams with staged environments3 plus (main, pre-prod, production)Days to weeksStaged rollouts, compliance gates
Release FlowMicrosoft (Azure DevOps / VSTS)1 main, 1 branch per releaseOne release cycleSprint based teams, scheduled releases
Distributed maintainer treesLinux kernelNone shared, every maintainer has their ownVaries by subsystemHuge open source projects, email based review

2. Branch lifetime, side by side

Trunk based (Google, Meta) branches live hours, merge same day GitHub Flow (Shopify, startups) branches live 1 to 3 days GitLab Flow (staged envs) feature branch, then flows through env branches Release Flow (Microsoft) topic branch short, release branch lives one cycle GitFlow (enterprise, SDKs) develop branch lives for the whole release cycle, weeks to months Linux kernel (maintainer trees) no shared branch: patches by email, pulled through a tree of maintainers hours days weeks or months

Fig. 1: Typical branch lifetime by strategy. Shorter lived branches need stronger automated testing to stay safe.

2.1 Real numbers from each company

35,000+engineers on one trunk at Google
11.6 secaverage time between Amazon production deploys
5 to 10commits per batch in Shopify's merge queue
71,000lines of dead flag code removed by Uber's Piranha tool, 2017 to 2019

These numbers describe very different things. Google's number is about how many people share one branch. Amazon's is about deploy frequency, which is a release practice, not a branching one. Shopify's is about how a merge queue controls risk on top of a simple GitHub Flow. They are grouped here because all four only work because of the branching model underneath them: one place for everyone's code to land, reviewed and tested before it lands.

3. Trunk based development

GoogleMetaNetflixAmazonUberMost high performing SaaS teams

Everyone commits to one branch, called trunk or main. If branches exist at all, they live for hours and exist only for code review and CI, not for feature development that spans days.

How it works

  • One long lived branch. No develop branch, no long lived feature branches.
  • Unfinished work merges to trunk hidden behind a feature flag, not left on a branch.
  • A flag can be rolled out gradually: internal staff first, then 1% of traffic, then 10%, watching error rates at each step.
  • Heavy investment in CI, automated tests and in some cases a custom build system (Google uses Bazel internally).

Company specifics

  • Google runs its entire codebase, used by more than 35,000 engineers, in one Perforce based monorepo called Piper. Most engineers work through CitC (Client in the Cloud), a cloud backed filesystem where only changed files live in your workspace and unmodified files are not copied locally.
  • Meta is commonly cited alongside Google and Netflix as a trunk based, feature flag heavy organization.
  • Netflix built Spinnaker, an open source continuous delivery platform, to manage canary releases, red/black deploys and rollbacks across its microservices.
  • Amazon pushes a production deploy on average every 11.6 seconds across its services. This comes from "two pizza teams", groups small enough to feed with two pizzas, usually 10 people or fewer, who own one service end to end and deploy it independently with no central approval queue.

Why this needs feature flags, and the catch

Trunk based development only works if unfinished code hidden behind a flag never actually breaks anything. Flags are cheap to add and expensive to remove, so they build up. Minware and GrowthBook both describe this as flag debt: a codebase that accumulates hundreds of active toggles, many for features that shipped months ago, each one adding an untested combination of code paths.

The $440 million case for cleaning up flags: in 2012, Knight Capital deployed new trading code to 7 of its 8 servers. The 8th server kept an 8 year old flag that used to trigger a retired algorithm called Power Peg. The new code reused that same flag number for an unrelated purpose instead of allocating a new one. Orders sent with the repurposed flag woke up the dead algorithm on the 8th server, which bought high and sold low for 45 minutes before anyone found it, losing the firm $440 million and leading to its acquisition within the year.

Uber's fix: a tool called Piranha scans source code, finds stale flags, and automatically generates a diff that deletes the dead code paths for review. Between December 2017 and May 2019 it generated cleanup diffs for 1,381 flags and removed 71,000 lines of code, with 65% of its generated diffs landing with no manual changes needed. It is open source and supports Java, Objective-C and Swift.

Pros

  • Few or no long merges, so merge conflicts stay small.
  • Deploys can happen many times a day.
  • Feature flags let you decouple shipping code from releasing a feature to users.

Cons

  • Needs strong CI and automated test coverage before it is safe.
  • Feature flag sprawl becomes its own maintenance problem if flags are never cleaned up.
  • Requires tooling investment most small teams do not have (Google and Meta built their own).

4. GitHub Flow

GitHubShopifyMost startups

One main branch that is always deployable. Every change gets a short branch, a pull request, review, then a merge to main that triggers a deploy.

How it works

  • Branch off main for any change, however small.
  • Open a pull request early so others can see and comment on the work.
  • Merge once it passes review and CI. Main is always the source of truth for production.
  • No release branches, no fixed release schedule. You ship when you merge.

Company specifics

  • Shopify runs a tweaked GitHub Flow where main is the state of production. Their Shipit tool batches 5 to 10 merged commits into a deploy at a time through a merge queue, which makes it easier to spot the exact commit that caused a problem and roll it back.
  • At Shopify, merging a pull request is close to the last human step. A deploy bot batches merges and triggers deploys on a schedule, so you do not log into a server to push code.

Pros

  • Simple to explain to a new hire in five minutes.
  • Works well with continuous deployment.
  • Low ceremony, no release branch bookkeeping.

Cons

  • No built in way to support multiple versions in production at once.
  • Still needs decent CI, or main breaks for everyone.
  • Does not fit products that ship on a fixed schedule to an app store or on physical media.

5. GitFlow

Published by Vincent Driessen, 2010Enterprise softwareSDKsRegulated industries

Two long lived branches, main and develop, plus supporting branches for features, releases and hotfixes.

How it works

  • develop holds work in progress for the next release.
  • Feature branches come off develop and merge back into it.
  • A release branch is cut from develop to stabilize a version, then merged into both main and develop.
  • Hotfix branches come off main for urgent production fixes, then merge back into both main and develop.
  • main only ever holds tagged, released versions.

Pros

  • Clear structure for supporting several released versions at once.
  • Good fit for software with scheduled, versioned releases: SDKs, firmware, desktop apps.
  • Easy to explain the state of any given commit: feature, release candidate, or shipped.

Cons

  • Long lived develop branch drifts far from main, so merges get painful.
  • GitLab's own documentation calls it "annoying" and full of unnecessary ceremony for teams that deploy often.
  • The --no-ff merge habit many teams pair with GitFlow produces a tangled commit history.
  • Most teams that ship web products many times a day have moved away from it.
Still a reasonable choice for: versioned SDKs, embedded or firmware projects, and regulated industries that must support several shipped versions in parallel for years. Not a good default for a typical SaaS web app in 2026.

6. GitLab Flow

GitLabTeams with staged environmentsCompliance heavy orgs

Takes GitHub Flow's single main branch and adds environment branches on top, such as pre-production and production.

How it works

  • Feature branches merge into main, same as GitHub Flow.
  • main is then merged downstream into one or more environment branches in order.
  • Code only reaches production after it has flowed through every environment branch in between.
  • Each environment branch must always be deployable to its matching environment.
  • A hotfix follows the same path as any other change. It is not allowed to skip straight to the production branch, which is the main difference from teams that quietly patch production directly and let it drift.

GitLab designed this model for its own product, where every merge request already runs through staged pipelines before a release. The appeal over plain GitHub Flow is that "deployable" is proven at each stage, not assumed. The cost is that a change takes longer to reach users, since it has to clear every environment branch in order rather than going straight from merge to production.

Pros

  • Gives you an explicit, auditable gate between each environment.
  • Keeps the simplicity of one main development branch.
  • Fits teams that need a paper trail for what went where and when.

Cons

  • More branches to keep green than plain GitHub Flow.
  • Promoting code through environment branches adds latency between merge and production.
  • Still needs discipline to stop environment branches from drifting apart.

7. Release Flow

Microsoft, Azure DevOps / VSTS team

Trunk based day to day, but with a dedicated release branch cut for each shipped release instead of deploying straight from main.

How it works

  • Developers work in short lived topic branches off main, same as trunk based development.
  • main stays buildable at all times.
  • When a team is ready to ship, usually at the end of a sprint, they cut a new release branch from main and name it after the release.
  • Release branches do not merge back into main. Important fixes are cherry picked back instead.
  • Unlike GitHub Flow, the team does not continuously deploy main straight to production. They release on a sprint cadence from the release branch.

Pros

  • Keeps the simplicity of trunk based development for daily work.
  • Gives a clean, named artifact for each release, useful for a scheduled release cadence.
  • Hotfixing a specific release is straightforward: branch from that release branch.

Cons

  • Cherry picking fixes back from a release branch is manual and easy to forget.
  • Not true continuous deployment, since production only updates when a release branch ships.
  • Needs a scheduling cadence (sprints) to make sense. Awkward for teams that ship whenever a change is ready.

8. Distributed maintainer trees

Linux kernel

No shared branch at all. Patches travel by email, reviewed on mailing lists, and pulled upward through a tree of maintainers.

How it works

  • Each subsystem has its own maintainer and its own git tree, plus a mailing list.
  • Contributors send patches as emails using git format-patch and git send-email, not pull requests.
  • A subsystem maintainer typically keeps two branches: one tracking Linus's tree for the current release, one for the next release.
  • Maintainers pull from other maintainers below them. For example the networking tree is built from patches collected first in driver specific trees. This chain is rarely longer than two or three links.
  • Linus Torvalds pulls from top level maintainers to assemble each release.
  • A new kernel release opens with a two week merge window where new work is pulled in, then goes through weekly release candidates, usually rc1 through rc7, that only accept fixes, not new features.

There is no equivalent of a pull request button and no single shared integration branch anyone writes directly to. The entire coordination mechanism is a chain of trust: Linus trusts his top level maintainers to only send him things their own reviewers have already checked, and so on down the tree.

Pros

  • Scales to thousands of contributors with no central write access needed.
  • Clear ownership: every subsystem has a named maintainer responsible for its tree.
  • Review happens in the open on public mailing lists.

Cons

  • Steep learning curve for anyone used to pull requests on GitHub.
  • Email based patch review tooling is unfamiliar to most engineers today.
  • Only works because of decades of process discipline. Hard to copy wholesale into a company.

9. Which one should you actually use

Your situationReasonable choice
Early stage startup, one small team, web appGitHub Flow
Growing SaaS company, strong CI, want to ship many times a dayTrunk based development with feature flags
Enterprise product with several supported versions in the fieldGitFlow, or Release Flow if you also want trunk based daily work
Regulated industry needing an audit trail between environmentsGitLab Flow
Sprint based team shipping on a fixed cadenceRelease Flow
Huge open source project with thousands of outside contributorsDistributed maintainer trees, Linux kernel style
You do not have reliable CI or automated tests yetDo not pick trunk based development yet. Fix testing first, use GitHub Flow in the meantime
Rule of thumb: the shorter your branches live, the more you depend on automated tests to catch problems instead of a human reviewing a big diff. Match your branching strategy to how much you trust your test suite, not to what a famous company does.
Why copying Google exactly usually fails: Google's trunk based model works because of roughly two decades of investment in Piper, CitC, Bazel, and a code review culture backed by a huge testing infrastructure team. A 15 person startup that adopts "one branch, no exceptions" without that infrastructure usually ends up with a trunk that is red half the time and a team that has quietly started avoiding merges, which is worse than GitHub Flow with real pull requests. Copy the principle, build the automated testing first, then earn the short branch lifetimes.

9.1 Common mistakes, by strategy

StrategyMistake teams actually makeFix
Trunk basedMerging to trunk without a flag, so half finished work goes live by accidentRequire a flag check in code review for any incomplete feature
Trunk basedLetting feature flags pile up with no owner or expiry dateAssign an owner and expiry at creation, run periodic cleanup like Uber's Piranha
GitHub FlowLetting pull requests sit unreviewed for days, so main drifts and merges get riskySet a review SLA, or add a merge queue like Shopify's to control the rate of change
GitFlowUsing --no-ff everywhere, producing an unreadable merge commit historyRebase feature branches before merging, or accept a flatter history on release branches only
GitFlowLetting develop and main drift for months before a release branch is cutCut release branches more often, even if the release itself ships less often
GitLab FlowEnvironment branches silently diverge because someone hotfixes production directlyRequire every hotfix to flow back through the same branch chain as a normal change
Release FlowForgetting to cherry pick a critical fix back into the release branch after merging it to mainTrack open release branches and their pending cherry picks in your issue tracker, not from memory

10. Moving from GitFlow to trunk based development

A common real world path. Do not flip the switch overnight. Order matters.

  1. Get CI green first. Every commit to main must run the full automated test suite before anyone touches branching. Skipping this step is how teams end up with a broken trunk and a worse experience than GitFlow gave them.
  2. Shrink your feature branches. Before removing develop, start merging feature work in smaller, more frequent pull requests. A branch that lives two days is a much smaller leap than one that lives two weeks.
  3. Introduce feature flags for anything unfinished. Pick one flag system (LaunchDarkly, GrowthBook, Unleash, or an in-house one) before you need ten of them.
  4. Retire develop. Merge it into main one last time, then stop creating new branches from it. New feature branches now come off main directly.
  5. Assign flag owners and expiry dates from day one. This is the step most teams skip, and it is exactly how flag debt builds up later.
  6. Keep a release branch strategy if you still need versioned releases. You can combine trunk based daily work with Microsoft's Release Flow pattern, cutting a release branch when you actually ship, instead of going all the way to continuous deployment on day one.

11. Quick glossary

TermMeaning
TrunkThe single long lived branch everyone merges into. Also called main.
Feature flagA runtime switch that turns a piece of code on or off without a new deploy.
MonorepoOne repository holding the code for many projects or services, as opposed to one repository per project.
Cherry pickCopying one specific commit from one branch onto another, without merging the whole branch.
Canary releaseRolling a new version out to a small slice of traffic first, then expanding if nothing breaks.
Merge queueA system that batches and orders merges to main instead of letting every pull request merge the instant it is approved.

12. Monorepo versus polyrepo, and how it changes branching

The branching strategy a company can realistically run is tied to how its code is split across repositories.

MonorepoPolyrepo
ExampleGoogle's Piper, Meta's codebaseMost startups, one repo per service
Cross project changesOne commit can touch many projects at once and is tested togetherNeeds coordinated releases or careful versioning across repos
Trunk based developmentNatural fit. One trunk, one history, one CI run proves the whole tree still worksWorks, but each repo needs its own CI and its own discipline
Tooling neededCustom build systems at Google and Meta scale, or Bazel for smaller setupsStandard per-language tooling usually suffices
Access controlHarder to restrict who can see or change whatNatural repo level boundaries

A monorepo does not force trunk based development, and trunk based development does not require a monorepo. But in practice the two show up together often, because a monorepo removes a lot of the cross repository coordination that makes long lived branches tempting in the first place.

13. Signs your branching strategy is failing

Teams rarely change their branching model because the model is wrong in theory. They change it because a symptom becomes painful enough. The table below lists the symptoms that show up most often and what they usually point to.

SymptomWhat it usually meansWhat to try first
Merges take days and create many conflictsBranches live too long, so the codebase moves on underneath themShorten branch lifetime. Merge smaller pull requests more often, even before a feature is complete, behind a flag.
Main is often red and nobody trusts itCI is too slow, too flaky, or does not cover the paths people changeFix the test pipeline before changing the branching model. Quarantine flaky tests and track their count.
Production hotfixes never make it back to the main lineHotfixes go straight to the production branch and drift away from the rest of the codebaseRequire every hotfix to flow back through the same path as a normal change, even if that adds a few minutes.
Release day is stressful and gets delayedToo many changes accumulate between releases, and integration is left to the endShip more often in smaller releases. If you must keep a release branch, cut it earlier and cherry pick fixes deliberately.
Feature flags keep growing and nobody removes themFlags are created without owners or expiry datesAssign an owner and an expiry date at creation. Schedule removal as part of finishing each feature.
Engineers avoid merging because it is riskyTrust in the pipeline is low, so people batch changes and delay integrationAdd a merge queue to control the rate of change, and make rollbacks fast and routine so merging is no longer a commitment.

The pattern across these symptoms is consistent. Most branching pain is a feedback problem: changes wait too long to be integrated and tested. A strategy with long branches hides that problem until release. A strategy with short branches exposes it early, which is uncomfortable but cheaper to fix.

14. When a migration is worth the cost

Moving from one strategy to another has real costs: retraining, tooling changes, a period of mixed practices, and sometimes a temporary drop in velocity. A migration is worth it when the current model blocks something the business needs, not because a newer model sounds better. Three situations usually justify the cost.

  • Release frequency is capped by the branching model. If the team wants to ship daily but the process only allows monthly releases, the branching model is the constraint.
  • The number of supported versions has shrunk. A product that once needed several long lived release lines may now need only one. Keeping the old model then adds overhead with no benefit.
  • Integration problems dominate the incident log. When a large share of production incidents trace to merges and divergent branches, the cost of the current model is measurable.

If none of these apply, improving the existing strategy usually pays more. Tighter CI, faster reviews, and a disciplined flag lifecycle often give most of the benefit of a migration with much less disruption.

15. Android branching strategies for efficient development

Everything above assumes a product you can redeploy at will. A web team that ships a bad build can roll back in minutes, and every user gets the fix on their next page load. An Android team lives in a different world. Once a build reaches users' phones, it stays there until each user updates. The Play Store sits between you and your users. Several versions of your app are always live at the same time. These facts change which branching strategy works, so mobile deserves its own section.

2100000000the highest versionCode Google Play accepts, so the number must be planned
100maximum testers on the internal testing track, which updates within minutes
N versionslive at once in the wild, because users update on their own schedule
Halt, not undoa staged rollout can be stopped, but users who already updated keep the build

15.1 Why mobile changes the rules

Five constraints shape every Android branching decision.

  1. You cannot roll back a shipped binary. Google's documentation is explicit: halting a staged rollout stops new users from receiving the version, but users who already updated stay on it. The only fix is a new release with a higher version code. So a bad build costs you at least one full release cycle, not a quick redeploy.
  2. Many versions live at once. Some users update the same day, some in a month, some never. Your backend must keep serving old app versions, and a bug fix may be needed on more than one line.
  3. Every upload needs a unique, increasing version code. Play rejects an upload whose versionCode was used before, and the system uses the code to block downgrades. Two branches building with the same code is a real release blocker, not a cosmetic issue.
  4. Release is a pipeline, not a moment. A build moves through internal, closed and open testing tracks, then a staged production rollout. That pipeline takes days, so the code being released is always older than trunk.
  5. Builds are slow and signed. A clean Gradle build of a large app can take many minutes, and release builds must be signed. Long builds punish long lived branches, because every merge conflict costs a full rebuild to verify.

Put together, these constraints push mobile teams toward one pattern more than any other: trunk based development for everyday work, plus short lived release branches cut on a fixed schedule. This is the release train, and it is the backbone of the rest of this section.

15.2 The release train model

Picture a real train station. A train leaves every two weeks at a fixed time. If your feature is ready, it boards. If it is not, it waits for the next train. The train never waits for a passenger. That is the whole idea. Release dates are fixed, and scope flexes, never the other way round.

In branch terms it looks like this.

  • main is the trunk. All feature work merges here through short lived branches and pull requests, guarded by CI. Main is always buildable and always shippable to the internal track.
  • On a fixed day, a release branch is cut. For example release/4.12 is created from main every second Tuesday. From that moment, main moves on to 4.13 and the release branch only stabilizes.
  • Only fixes enter the release branch. No new features. Fixes land on main first and are cherry picked into the release branch, so main never loses a fix.
  • The release branch ships through Play tracks. Internal, then closed, then production with a staged rollout.
  • The release branch dies after the next train ships. It is kept only as long as that version needs hotfixes.
main short lived feature PRs merge daily release/4.12 cut (Tue, week 0) cherry picks release/4.13 cut (week 2) internal closed beta 1 percent 10 percent 50 percent 100 percent each release branch walks the Play tracks left to right; a halt freezes it at the current step

Fig 15.1. The Android release train: daily merges to main, a release branch cut on a fixed day, fixes cherry picked from main, and a staged walk through Play testing tracks.

Why it works

  • Predictable dates for product, QA, marketing and support
  • Main never freezes, so developers keep shipping
  • A missed feature costs two weeks, not a slipped release
  • Small, frequent releases are easier to debug than big ones

What it costs

  • Needs feature flags for anything larger than one train
  • Cherry picks need discipline and tooling
  • A release captain role must rotate every train
  • Two or three release branches can be alive during overlap

15.3 How the classic strategies fit Android

StrategyFit for AndroidWhy
Trunk based + release branches (release train)Best defaultMatches the store pipeline. Main moves fast, release branches stabilize. Used widely by large mobile teams.
GitHub FlowSmall apps onlyMain is the release. Works when one or two developers ship rarely, but there is no place to stabilize a build while main keeps moving.
GitFlowWorkable, often heavyThe develop branch plus release branches mirrors the store pipeline, which is why many mobile teams started here. The long lived develop branch drifts and slows merges as the team grows.
GitLab Flow (environment branches)Rarely a fitEnvironments are Play tracks, not branches. Promoting a build between tracks happens in Play Console, so branch per environment duplicates what Play already does.
Release FlowVery goodEssentially the same as the release train: trunk work, named release branches, fixes on main first then cherry picked.
Long lived version branchesOnly for SDKs and white label appsNeeded when you support several major versions in parallel, for example an SDK that other apps embed, or an enterprise build pinned by a customer.

15.4 Branch naming and protection

Clear names let tools act on branches automatically. A common, simple scheme:

BranchLifetimeWho mergesProtection
mainForeverAnyone, through reviewed PRsRequired CI checks, at least one review, no force push
feature/<ticket>-short-nameHours to 2 daysAuthorNone; deleted after merge
release/<major.minor>One to two trainsRelease captain onlyLocked to cherry picks, required CI, codeowner approval
hotfix/<major.minor.patch>HoursRelease captainSame as release branches

On GitHub, branch protection rules and a CODEOWNERS file enforce this. A rule pattern such as release/* can require a passing build and an approval from the release owners group, so a casual merge into a release branch is impossible.

15.5 Build variants are not branches

New Android teams sometimes create a branch per flavor: one branch for the free app, one for the paid app, one for a white label customer. This is a trap. The branches drift, every fix must be applied several times, and before long nobody knows which branch has which fix.

Gradle already solves this. Product flavors and build types produce many variants from one source tree. Flavor specific code lives in source sets like src/free/ and src/paid/, and shared code lives in src/main/. One branch builds every variant.

android {
    flavorDimensions += "tier"
    productFlavors {
        create("free") {
            dimension = "tier"
            applicationIdSuffix = ".free"
        }
        create("paid") {
            dimension = "tier"
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
        }
    }
}

The rule of thumb is simple. Use a branch for time (what is shipping in which release). Use a flavor for shape (which version of the product a given audience gets). Use a feature flag for exposure (which users see a feature right now).

15.6 versionCode strategy across branches

Because every upload needs a unique and increasing versionCode, and Play caps it at 2100000000, the numbering scheme must be designed up front. Two schemes are common.

Scheme A, derive it from the version name. Encode major, minor and patch into digits, for example major * 10000 + minor * 100 + patch. Version 4.12.3 becomes 41203. Hotfixes on old release branches stay below newer releases automatically, because a patch on 4.12 can never exceed 4.13.0.

Scheme B, use the CI build number. Every CI build gets the next integer. This is simple and always unique, but a hotfix built today on an old branch could get a higher code than a newer release built yesterday. Since devices receive the highest compatible version code they are eligible for, that can surprise you, so scheme B needs care when more than one line is active.

// app/build.gradle.kts, Scheme A
val major = 4
val minor = 12
val patch = 3
android {
    defaultConfig {
        versionCode = major * 10000 + minor * 100 + patch
        versionName = "$major.$minor.$patch"
    }
}
Why the cap matters: a scheme that multiplies by large factors, or prefixes dates like 20261014 plus a counter, can run into the 2100000000 ceiling years before you expect. Pick a scheme you could live with for a decade.

15.7 Feature flags make the train possible

A feature that takes six weeks cannot sit on a branch for three trains without rotting. Instead it merges to main in small pieces behind a flag that defaults to off. The code ships inside every release, dormant. When it is ready, the flag turns on remotely, often for 1 percent of users first.

On Android this is usually done with a remote configuration service, such as Firebase Remote Config or an in house flag service, fetched at app start and cached. Three rules keep flags healthy on mobile:

  • Default safe. The compiled default must be the safe value, because a user who is offline or on a stale cache gets the default.
  • Remove flags after launch. Old app versions in the wild still check the flag, so you remove the code path in a new release, and only delete the remote value once almost nobody runs the old versions.
  • A flag is a kill switch. Since you cannot roll back a binary, the ability to turn a broken feature off remotely is often the fastest fix you have. Wrap risky features in flags even when they fit in one train.

15.8 Play tracks and staged rollout

Play Console provides the environments that web teams build with branches. A release branch build moves through them in order.

StageWho gets itTypical goal
Internal testingUp to 100 testers added by email; updates arrive within minutesSmoke test the exact signed bundle on real devices
Closed testingEmail lists or Google Groups; lists of up to 2,000 users eachDogfood with employees and beta users for a few days
Open testingAnyone who opts in; visible on PlayBroader device coverage before production
Production, stagedA percentage you choose, raised manuallyWatch crashes and ANRs at 1, 10, 50, then 100 percent

A few details from Google's documentation matter for branching. Staged rollouts work only for updates, not a first publish. The percentage never rises by itself, so someone must decide to raise it. If you start a new release before finishing the previous one, it reaches the same user group up to the new percentage. And halting stops new installs of that version, but users who already got it keep it. That last point is why the fix always comes from the release branch as a new, higher version code.

Gate each step on numbers, not on time. A release captain can define thresholds such as crash free sessions and ANR rate compared with the previous release, and only raise the percentage when the new build is at least as healthy.

15.9 The hotfix flow, step by step

Suppose 4.12.0 is at 10 percent and the crash rate jumps. Here is a calm, repeatable response.

  1. Halt the rollout in Play Console. No new users get 4.12.0.
  2. If the bug is in a flagged feature, turn the flag off. This protects users who already have 4.12.0 within minutes, no new build needed.
  3. Fix on main first. Open a PR against main with the fix and a test that reproduces the crash. Merge it.
  4. Cherry pick into the release branch. git cherry-pick -x <sha> onto release/4.12. The -x flag records the original commit in the message, which keeps an audit trail.
  5. Bump the patch. 4.12.1, versionCode 41201, higher than 41200.
  6. Build, sign and upload from the release branch through CI, to internal testing first.
  7. Resume the staged rollout with the new build. Start low again and climb on healthy numbers.
Never fix only on the release branch. If the fix lands on release/4.12 but not on main, the bug comes back in 4.13. Fix forward on main, then cherry pick back. Teams that automate this check catch the problem in CI rather than in production.

For a truly critical bug, the in-app updates API in the Play Core libraries offers an immediate update flow: a full screen prompt that requires the user to update and restart before continuing. The flexible flow downloads in the background while the user keeps working. These help shrink the long tail of users stuck on a broken version.

15.10 CI for an Android release train

Fast feedback keeps branches short. A practical pipeline has three lanes.

  • Every PR to main: lint, unit tests and a debug build. Use the Gradle build cache and configuration cache so most PRs finish in a few minutes.
  • Every merge to main: a signed internal build uploaded to the internal testing track, so anyone can install trunk on a phone the same day.
  • Every push to a release branch: the full suite including instrumented UI tests on emulators or a device farm, a release build signed with the upload key, and an upload to internal or closed testing.
# .github/workflows/android.yml (simplified)
name: android
on:
  pull_request:
    branches: [ main ]
  push:
    branches: [ main, "release/**" ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: "17" }
      - uses: gradle/actions/setup-gradle@v4
      - run: ./gradlew lint testDebugUnitTest assembleDebug
      - name: Release bundle (release branches only)
        if: startsWith(github.ref, 'refs/heads/release/')
        run: ./gradlew bundleRelease
        env:
          UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}

Keep signing keys out of the repository. With Play App Signing, Google holds the app signing key and your CI only holds an upload key, which can be reset if it leaks. Store the upload key and its passwords as CI secrets, and only expose them to release branch jobs.

15.11 Modularization keeps trunk healthy

Branching strategy and code structure are linked. A single giant app module means every change rebuilds everything, every PR touches the same files, and merge conflicts pile up. Splitting the app into Gradle modules, for example one per feature plus shared core modules, gives three branching benefits.

  • Smaller, faster builds. CI only rebuilds and tests the modules a change touched, so PRs stay quick and branches stay short.
  • Fewer conflicts. Teams work in their own modules, so two features rarely edit the same file.
  • Clear ownership. CODEOWNERS maps each module directory to a team, so reviews route automatically.

15.12 The release captain

A train needs a driver. Most mobile teams rotate a release captain each cycle. The captain cuts the branch on schedule, approves cherry picks, watches vitals, raises or halts the rollout, and writes release notes. Rotating the role spreads knowledge, and a written runbook keeps each train the same. A short checklist the captain follows:

  1. Cut release/X.Y from green main on the scheduled day.
  2. Bump main's version to the next minor.
  3. Ship to internal, then closed testing; run the regression pass.
  4. Accept only fixes that are already merged on main; cherry pick with -x.
  5. Start production at a small percentage; raise only on healthy vitals.
  6. Tag the shipped commit, for example v4.12.0, and hand over to the next captain.

15.13 How Android itself is developed

The Android platform, the Android Open Source Project, is a different beast from an app. It spans hundreds of Git repositories stitched together by the repo tool and a manifest, and it uses release branches and tags per platform release. In 2025 Google said it would do more of its platform development internally and publish source to AOSP at release points. The lesson for app teams is the same one: development happens on a moving trunk, and releases are snapshots taken from it and stabilized separately.

15.14 Common Android branching mistakes

  • A branch per flavor or per customer. Use product flavors and source sets instead.
  • Long feature branches waiting for "the right release". Merge behind a flag and let the train carry it.
  • Fixing only on the release branch. The bug returns in the next version.
  • Unplanned versionCodes. Two branches building the same code block an upload at the worst moment.
  • Treating a halted rollout as a rollback. Users who updated still have the bad build. Plan the fix release and the flag switch together.
  • Signing keys in the repository. Use Play App Signing and CI secrets.
  • Raising rollout percentage on a timer. Raise it on vitals.

15.15 Which setup should your Android team use

TeamRecommended setup
Solo or 2 to 3 developers, monthly releasesGitHub Flow with a tag per release, internal track for testing, staged rollout for production
5 to 30 developers, regular releasesTrunk based with a two week release train, feature flags, a rotating release captain
Large org, several app teams, weekly releasesTrunk based monorepo, modularized Gradle build, weekly train, automated cherry pick checks and vitals gates
SDK or library used by other appsTrunk plus long lived version branches for each supported major version

If your team is somewhere in the middle, start with the release train. It is the one model that respects every mobile constraint at once: binaries you cannot recall, many versions live together, increasing version codes, and a store pipeline that takes days.

FAQ

Is trunk based development the same as continuous deployment?

No. Trunk based development is about where code lives before it merges. Continuous deployment is about what happens after it merges. You can do trunk based development and still deploy on a schedule, as Microsoft's Release Flow shows.

Can a big company use GitHub Flow?

Yes, Shopify does, with extra tooling (a merge queue and a deploy bot) layered on top to control the rate and safety of merges. The core model stays simple even as the company scales.

Why do people say GitFlow is outdated?

Most criticism targets using it for a web product that deploys often. A long lived develop branch drifts from main and merges get painful. It still fits products with multiple supported released versions, such as SDKs or firmware.

What do monorepo and branching strategy have to do with each other?

A monorepo makes trunk based development easier, since every team's code sits in one place and can be tested together on every commit. It is possible to use trunk based development with multiple repos too, but a monorepo like Google's Piper removes a lot of the coordination overhead.

Do I need feature flags to do trunk based development?

For anything beyond a small feature, yes. Without a flag, half finished work merged to trunk either ships to users before it is ready or has to wait on a branch, which defeats the point of trunk based development.

How do I stop feature flags from piling up?

Assign an owner and an expiration date to every flag when it is created, and treat flag removal as a normal part of finishing a feature, not optional cleanup. Uber's open source Piranha tool automates finding and removing stale flags for Java, Objective-C and Swift codebases.

Can I mix strategies, for example trunk based work with release branches?

Yes, this is exactly what Microsoft's Release Flow does. Day to day work is trunk based with short lived topic branches, but instead of deploying straight from main, a named release branch is cut per release and fixes are cherry picked back into it when needed.

What does the Linux kernel do differently from everyone else here?

Every other strategy in this article assumes one shared repository with write access controlled by permissions. The kernel has none of that. Patches move as emails, each maintainer owns their own tree, and the only thing holding it together is a chain of people who trust the reviewer below them. It works at a scale almost nothing else matches, but it took decades of process to get there.

What is the best branching strategy for an Android app?

For most teams, trunk based development with short lived feature branches plus a release branch cut on a fixed schedule, often called a release train. It matches how Play releases work: a build stabilizes on its release branch while main keeps moving.

Can I roll back an Android release like a web deploy?

No. Halting a staged rollout stops new users from getting the version, but users who already updated keep it. You fix forward with a new release that has a higher versionCode, and you use feature flags as remote kill switches in the meantime.

Should free and paid versions of my app live on separate branches?

No. Use Gradle product flavors and flavor source sets on one branch. Branches are for time (which release), flavors are for shape (which product variant), and flags are for exposure (which users see a feature).

Takeaways

  • Trunk based development (Google, Meta, Netflix) trades long branches for strong CI and feature flags.
  • GitHub Flow (GitHub, Shopify, startups) is the simplest model that still supports continuous deployment.
  • GitFlow still earns its keep for versioned SDKs and regulated software, but is a poor default for a fast moving web product.
  • GitLab Flow adds explicit environment gates on top of a simple main branch.
  • Microsoft's Release Flow keeps trunk based daily work but ships from a named release branch per cycle.
  • The Linux kernel shows a working model with no shared branch at all, built on mailing lists and maintainer trust.
  • Android and other mobile teams cannot roll back shipped binaries, so a release train with feature flags, planned versionCodes and staged Play rollouts is the safest default.
  • Choose based on release cadence, team size and test automation maturity, not on which company is trendiest.

References

← browse the archive