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.
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.
| Strategy | Used by | Main branches | Branch lifetime | Best for |
|---|---|---|---|---|
| Trunk based development | Google, Meta, Netflix, most SaaS at scale | 1 (trunk / main) | Hours | Strong CI, feature flags, frequent releases |
| GitHub Flow | GitHub, Shopify, most startups | 1 (main) | Days | Web apps with continuous deploy |
| GitFlow | Enterprise software, SDKs, regulated industries | 2 (main, develop) | Weeks to months | Versioned releases, multiple supported versions |
| GitLab Flow | Teams with staged environments | 3 plus (main, pre-prod, production) | Days to weeks | Staged rollouts, compliance gates |
| Release Flow | Microsoft (Azure DevOps / VSTS) | 1 main, 1 branch per release | One release cycle | Sprint based teams, scheduled releases |
| Distributed maintainer trees | Linux kernel | None shared, every maintainer has their own | Varies by subsystem | Huge open source projects, email based review |
2. Branch lifetime, side by side
Fig. 1: Typical branch lifetime by strategy. Shorter lived branches need stronger automated testing to stay safe.
2.1 Real numbers from each company
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.
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
developholds work in progress for the next release.- Feature branches come off
developand merge back into it. - A release branch is cut from
developto stabilize a version, then merged into bothmainanddevelop. - Hotfix branches come off
mainfor urgent production fixes, then merge back into bothmainanddevelop. mainonly 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
developbranch drifts far frommain, so merges get painful. - GitLab's own documentation calls it "annoying" and full of unnecessary ceremony for teams that deploy often.
- The
--no-ffmerge 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.
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. mainis then merged downstream into one or more environment branches in order.- Code only reaches
productionafter 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. mainstays 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
mainand 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
mainstraight 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-patchandgit 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 situation | Reasonable choice |
|---|---|
| Early stage startup, one small team, web app | GitHub Flow |
| Growing SaaS company, strong CI, want to ship many times a day | Trunk based development with feature flags |
| Enterprise product with several supported versions in the field | GitFlow, or Release Flow if you also want trunk based daily work |
| Regulated industry needing an audit trail between environments | GitLab Flow |
| Sprint based team shipping on a fixed cadence | Release Flow |
| Huge open source project with thousands of outside contributors | Distributed maintainer trees, Linux kernel style |
| You do not have reliable CI or automated tests yet | Do not pick trunk based development yet. Fix testing first, use GitHub Flow in the meantime |
9.1 Common mistakes, by strategy
| Strategy | Mistake teams actually make | Fix |
|---|---|---|
| Trunk based | Merging to trunk without a flag, so half finished work goes live by accident | Require a flag check in code review for any incomplete feature |
| Trunk based | Letting feature flags pile up with no owner or expiry date | Assign an owner and expiry at creation, run periodic cleanup like Uber's Piranha |
| GitHub Flow | Letting pull requests sit unreviewed for days, so main drifts and merges get risky | Set a review SLA, or add a merge queue like Shopify's to control the rate of change |
| GitFlow | Using --no-ff everywhere, producing an unreadable merge commit history | Rebase feature branches before merging, or accept a flatter history on release branches only |
| GitFlow | Letting develop and main drift for months before a release branch is cut | Cut release branches more often, even if the release itself ships less often |
| GitLab Flow | Environment branches silently diverge because someone hotfixes production directly | Require every hotfix to flow back through the same branch chain as a normal change |
| Release Flow | Forgetting to cherry pick a critical fix back into the release branch after merging it to main | Track 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.
- 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.
- 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. - Introduce feature flags for anything unfinished. Pick one flag system (LaunchDarkly, GrowthBook, Unleash, or an in-house one) before you need ten of them.
- Retire
develop. Merge it intomainone last time, then stop creating new branches from it. New feature branches now come offmaindirectly. - 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.
- 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
| Term | Meaning |
|---|---|
| Trunk | The single long lived branch everyone merges into. Also called main. |
| Feature flag | A runtime switch that turns a piece of code on or off without a new deploy. |
| Monorepo | One repository holding the code for many projects or services, as opposed to one repository per project. |
| Cherry pick | Copying one specific commit from one branch onto another, without merging the whole branch. |
| Canary release | Rolling a new version out to a small slice of traffic first, then expanding if nothing breaks. |
| Merge queue | A 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.
| Monorepo | Polyrepo | |
|---|---|---|
| Example | Google's Piper, Meta's codebase | Most startups, one repo per service |
| Cross project changes | One commit can touch many projects at once and is tested together | Needs coordinated releases or careful versioning across repos |
| Trunk based development | Natural fit. One trunk, one history, one CI run proves the whole tree still works | Works, but each repo needs its own CI and its own discipline |
| Tooling needed | Custom build systems at Google and Meta scale, or Bazel for smaller setups | Standard per-language tooling usually suffices |
| Access control | Harder to restrict who can see or change what | Natural 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.
| Symptom | What it usually means | What to try first |
|---|---|---|
| Merges take days and create many conflicts | Branches live too long, so the codebase moves on underneath them | Shorten branch lifetime. Merge smaller pull requests more often, even before a feature is complete, behind a flag. |
| Main is often red and nobody trusts it | CI is too slow, too flaky, or does not cover the paths people change | Fix the test pipeline before changing the branching model. Quarantine flaky tests and track their count. |
| Production hotfixes never make it back to the main line | Hotfixes go straight to the production branch and drift away from the rest of the codebase | Require 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 delayed | Too many changes accumulate between releases, and integration is left to the end | Ship 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 them | Flags are created without owners or expiry dates | Assign an owner and an expiry date at creation. Schedule removal as part of finishing each feature. |
| Engineers avoid merging because it is risky | Trust in the pipeline is low, so people batch changes and delay integration | Add 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.
15.1 Why mobile changes the rules
Five constraints shape every Android branching decision.
- 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.
- 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.
- 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.
- 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.
- 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.
mainis 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.12is 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.
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
| Strategy | Fit for Android | Why |
|---|---|---|
| Trunk based + release branches (release train) | Best default | Matches the store pipeline. Main moves fast, release branches stabilize. Used widely by large mobile teams. |
| GitHub Flow | Small apps only | Main is the release. Works when one or two developers ship rarely, but there is no place to stabilize a build while main keeps moving. |
| GitFlow | Workable, often heavy | The 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 fit | Environments are Play tracks, not branches. Promoting a build between tracks happens in Play Console, so branch per environment duplicates what Play already does. |
| Release Flow | Very good | Essentially the same as the release train: trunk work, named release branches, fixes on main first then cherry picked. |
| Long lived version branches | Only for SDKs and white label apps | Needed 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:
| Branch | Lifetime | Who merges | Protection |
|---|---|---|---|
main | Forever | Anyone, through reviewed PRs | Required CI checks, at least one review, no force push |
feature/<ticket>-short-name | Hours to 2 days | Author | None; deleted after merge |
release/<major.minor> | One to two trains | Release captain only | Locked to cherry picks, required CI, codeowner approval |
hotfix/<major.minor.patch> | Hours | Release captain | Same 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"
}
}
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.
| Stage | Who gets it | Typical goal |
|---|---|---|
| Internal testing | Up to 100 testers added by email; updates arrive within minutes | Smoke test the exact signed bundle on real devices |
| Closed testing | Email lists or Google Groups; lists of up to 2,000 users each | Dogfood with employees and beta users for a few days |
| Open testing | Anyone who opts in; visible on Play | Broader device coverage before production |
| Production, staged | A percentage you choose, raised manually | Watch 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.
- Halt the rollout in Play Console. No new users get 4.12.0.
- 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.
- Fix on main first. Open a PR against main with the fix and a test that reproduces the crash. Merge it.
- Cherry pick into the release branch.
git cherry-pick -x <sha>ontorelease/4.12. The-xflag records the original commit in the message, which keeps an audit trail. - Bump the patch. 4.12.1, versionCode 41201, higher than 41200.
- Build, sign and upload from the release branch through CI, to internal testing first.
- Resume the staged rollout with the new build. Start low again and climb on healthy numbers.
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.
CODEOWNERSmaps 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:
- Cut
release/X.Yfrom green main on the scheduled day. - Bump main's version to the next minor.
- Ship to internal, then closed testing; run the regression pass.
- Accept only fixes that are already merged on main; cherry pick with
-x. - Start production at a small percentage; raise only on healthy vitals.
- 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
| Team | Recommended setup |
|---|---|
| Solo or 2 to 3 developers, monthly releases | GitHub Flow with a tag per release, internal track for testing, staged rollout for production |
| 5 to 30 developers, regular releases | Trunk based with a two week release train, feature flags, a rotating release captain |
| Large org, several app teams, weekly releases | Trunk based monorepo, modularized Gradle build, weekly train, automated cherry pick checks and vitals gates |
| SDK or library used by other apps | Trunk 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
- Android Developers: Version your app versionCode rules and the 2100000000 maximum.
- Play Console Help: Set up open, closed or internal tests Testing tracks and their limits.
- Play Console Help: Release app updates with staged rollouts Percentages, halting and resuming.
- Android Developers: In-app updates Flexible and immediate update flows.
- Android Developers: Configure build variants Product flavors and source sets instead of per variant branches.
- Android Developers: Guide to app modularization Splitting an app into Gradle modules.
- Software Engineering at Google, Chapter 16 Google's trunk based, monorepo development model.
- Why Google Stores Billions of Lines of Code in a Single Repository Piper and CitC in detail.
- trunkbaseddevelopment.com Reference site for the trunk based model and feature flags.
- Successfully Merging the Work of 1000+ Developers, Shopify Engineering Shopify's merge queue and GitHub Flow at scale.
- Automatic Deployment at Shopify Shipit and automatic deploys after merge.
- A Successful Git Branching Model, Vincent Driessen The original GitFlow post, 2010.
- Atlassian: Gitflow Workflow Structured overview of GitFlow's branches.
- GitLab: What is Gitflow GitLab's own critique of GitFlow's complexity.
- GitLab Flow overview Environment branches and production gating.
- Release Flow: How We Do Branching on the VSTS Team Microsoft's own description of Release Flow.
- The Linux Kernel documentation: How the development process works Maintainer trees and the patch pull chain.
- gitworkflows(7) Git's own manual page describing the kernel style workflow.
- How Netflix Built Spinnaker Netflix's continuous delivery platform for canaries and rollbacks.