A general guide to branching in a multi-product monorepo

How many teams ship one product across surfaces without collisions.

A general guide to branching in a multi-product monorepo

A general guide to branching in a multi-product monorepo

Trains used to pass the end of our road. One track, different speeds, one timetable. That is the feeling a healthy monorepo aims for: one trunk people trust, a few sidings when needed, departures you can set your watch by.

This guide sets out a branching model for the whole frontend of a product, not just a channel. Web, Android, iOS, and the backend-for-frontend that serves them live together. The gateway shapes API contracts for the clients and belongs next to the code it feeds. When it moves with the apps, refactors get simpler, rollbacks stay boring, and features travel as one story.

The promise is speed without chaos. Distributed teams can land changes without ceremony because the rules are clear and automation does the heavy lifting. Coordinate by shared tags, tested contracts, and observability. Let dates vary by surface. It will not fix everything, but it removes friction and gives a steady path to ship across surfaces with fewer surprises. Treat it as a pragmatic default you can adopt as is or tune to fit.

Table of contents

  1. Why one product, many surfaces
  2. What we mean by the Gateway
  3. Branch roles at a glance
  4. Naming is governance
  5. Versioning without conflicts
  6. Alignment without lockstep, different cadences, one truth
  7. Invariants that keep you honest
  8. Scenarios with exact steps
  9. Decision table
  10. CI policy and branch protections
  11. Metrics that prove it works
  12. Anti-patterns and how to correct them
  13. Adoption plan
  14. Reference snippets
  15. Appendix, branch-name regex
  16. Conclusion

Why one product, many surfaces

Users see one product. Shipping together reduces bugs that only appear when journeys cross surfaces. Web can sprint. Native can queue at the store. That is fine. Coordinate by shared tags, tested contracts, and observability. Let dates vary by surface. Keeping client code and the gateway close helps. Contracts are visible, refactors land together, rollbacks stay simple. Most importantly, teams in different places can add value without a long trail of approvals because the model makes intent obvious and the rules do the handholding.

What we mean by the Gateway

The gateway is a frontend-facing adapter between clients and backend systems, often called a backend-for-frontend. It translates and composes data for the experience.

Typical duties: shape and aggregate responses for screens, adapt backend models to client contracts, pass through authentication context and enforce simple policies, rate limit and trace, and provide a thin anti-corruption layer so clients are insulated from backend churn.

Why keep it in the monorepo: contracts live next to clients so changes are visible, type generation and contract tests run in CI against the same commit, additive changes can ship with web today and flow to native later. This lowers the bar for new contributors who can change a screen and its contract in the same PR, with the pipeline catching mistakes.

Branch roles at a glance

  • main
    Always shippable. Green CI. Feature flags hide incomplete work. This is the shared reality that lets any team contribute without asking who owns the calendar.
  • release/{surface}/{yy}.{ww}.{build}
    One branch per surface, all cut from the same tag on main. Examples:
    release/web/25.40.0, release/android/25.40.0, release/ios/25.40.0, release/gateway/25.40.0.
    If the gateway is tightly coupled to a web change, include it in the web release. If it benefits from its own cadence, give it a surface of its own. Either way, contributors know exactly where to land fixes.

develop/{vertical}
Short-lived staging when a vertical or service needs to integrate several features before returning to main. Examples:
develop/catalogue, develop/offers, develop/checkout, develop/account, develop/marketing, develop/platform, develop/native-android, develop/native-ios, develop/gateway.
These sidings give local teams a safe place to coordinate work that needs a small bundle, then return to the trunk quickly.Quick audit:

git branch -r | grep 'develop/'

Naming is governance

Branch names are contracts between people and automation. Enforce them with push rules so policy is syntax, not opinion. Clear names mean new folks can find the right on-ramp without a tour guide.

  • Feature branches: f/{area}/{short-title}
    Examples: f/checkout/payment-timeout, f/catalogue/new-filtering, f/gateway/add-recommended-field
  • Develop branches: develop/{vertical}
    Examples: develop/checkout, develop/gateway, develop/marketing
  • Release branches: release/{surface}/{yy}.{ww}.{build}
    Examples: release/web/25.40.0, release/android/25.40.0, release/ios/25.40.0, release/gateway/25.40.0
  • System branches: revert-*, cherry-pick-* created by your git hosting UI

See the full regex in the appendix.

Versioning without conflicts

Let CI derive versions from time and commit. No brittle version files to touch, which means fewer merge tangles and fewer reasons to block someone else’s work.

Format: {major}.{minor}.{build}.{revision}-{hash}

  • major is last two digits of the year of the latest commit
  • minor is the ISO calendar week of that commit
  • build comes from branch context
  • revision is minutes since the start of that week for the latest commit
  • hash is the short git SHA

Release branches such as release/web/24.21.0 produce something like 24.21.0.3842-a1b2c3d.
Non-release branches use build = 0, for example 25.40.0.9123-f00ba7c.

Calendar-style versions are identifiers, not schedules. They tell you when an artifact was produced. They do not force every surface to ship on that day.

Alignment without lockstep, different cadences, one truth

Web, native, and the gateway can move at different speeds. The product stays coherent because everyone aligns on the same truths. That lets a team in one office cut a train while another team is asleep, and nobody steps on toes.

Principles: the truth lives on main and every surface cuts from it. Per-surface release branches are normal. Skipping trains is normal. Feature availability is a matrix so use flags per surface. Contract tests prevent surprises. Fast-follow paths are explicit. One set of release notes is grouped by tag.

Cheat sheet:

Surface Typical cadence Release branch example Can skip a train Hotfix path
Web weekly release/web/25.40.0 yes fix on release/web/..., then main
Android 1 to 4 weeks release/android/25.40.0 yes fix on release/android/..., then main
iOS 1 to 4 weeks release/ios/25.40.0 yes fix on release/ios/..., then main
Gateway as needed release/gateway/25.40.0 yes fix on release/gateway/..., then main

Invariants that keep you honest

  1. main stays sacred. Merge queue on. Required checks include compile, unit, smoke, static analysis, size budgets, and a fast end to end. The queue equalises access so small teams are not stuck behind loud ones.
  2. develop branches chase main daily. If drift exceeds one day, block merges to that develop branch until reconciled. This protects new contributors from integrating against stale code.
  3. release branches are fixes only. Scope freezes at cut time.
  4. everything reconciles to main. When a commit lands on any release branch, open a PR and merge that release branch back into main the same day. One ledger of record.
  5. release branches persist. Do not delete release branches during their hotfix window. Keep them open and protected. After the window, lock the branch read-only and keep the tags.

Scenarios with exact steps

Small, safe feature to main

When the change can be flagged and covered by CI. Ideal for local teams delivering small wins without booking a summit.

Steps: create f/<area>/<title>, develop behind a flag, rebase on main daily, open PR to main, pass checks and approvals, merge via queue with the flag off by default.
Exit: build green, smoke green, flag off in main at merge time.

Vertical integration on develop/{vertical}

When several features must land together for a vertical or service.

Steps: branch from main, rebase daily, PR into develop/<vertical> and run vertical e2e there, then raise a short PR from develop/<vertical> back to main and merge quickly.
Exit: develop branch no more than one day behind main, e2e passing, small PR back to main. Teams coordinate locally without blocking others.

Shared library change with multiple consumers

Steps: branch from main and add a compatibility layer that defaults to old behaviour, land that to main first, migrate consumers on develop/<vertical> branches, remove the compatibility layer in a separate PR when all consumers are updated.
Exit: CI matrix green and owners approve removal. Smaller teams can modernise libraries without staging a company-wide rewrite.

Security bulletin

Steps: patch on main first, cherry-pick the minimal patch to any active release branches for web, Android, iOS, or gateway, tag new candidates, roll through rings.
Exit: forward fix in main, backports recorded in release notes. Clear steps mean any on-call group can act quickly.

Cut a train for web, Android, iOS, and gateway

Steps: ensure main is green and baseline metrics are steady, tag and branch per surface, freeze scope.
Result: each surface has a release branch from the same tag. Local teams plan their own rollout while staying aligned.

Hotfix during validation

Steps: create a hotfix branch from the relevant release branch, fix and merge into that release branch, tag .1, validate, then open a PR and merge the updated release branch back into main.
Result: release fixed, main reconciled via the merge back. No guessing which changes made it.

Incident after the release is live

Steps: branch from the surface release tag or release branch, fix and merge into that release branch, tag .2, roll out, then open a PR and merge the updated release branch back into main before the next train.
Result: main contains the full set of shipped changes. The next team to cut a train inherits the fix automatically.

Most recent release housekeeping

Steps: once the release is live and validated in production, merge the release branch back to main to capture any hotfixes. Keep the release branch open and protected for the hotfix window, for example 14 to 30 days. When the window closes, lock the branch read-only and leave the tags intact.
Result: release branches remain available for quick fixes without cherry-pick roulette. History stays legible.

Gateway contract change, additive

When a client needs a new field or endpoint and older clients must keep working.

Steps: implement in f/gateway/add-<field>, add contract tests so old and new shapes pass, land gateway change to main first, update web on f/<vertical>/... or develop/<vertical> and merge to main, let native adopt later in its next release.
Result: old clients unaffected, web uses the new field, native catches up later. A single PR can move both sides forward.

Gateway contract change, planned breaking

When a field must be removed or renamed.

Steps: add a compatibility window and support both shapes behind a flag, ship gateway with both shapes, migrate web and native, remove the old shape in a separate PR when supported clients have moved.
Result: no clients break, the compatibility code is removed on schedule. Teams can change contracts without freeze weeks.

Decision table

Scenario Base from Merge into Notes
Small feature main main Default path. Flag if risky.
Vertical integration main develop/{vertical} then main Siding, not a fork. One day max drift.
Shared lib change main main Add compatibility, remove later.
Gateway additive change main main Ship gateway first, clients follow.
Gateway planned breaking change main main Dual support, migrate, then remove.
Security bulletin main main and release/* Forward fix, then backport.
Release cut main release/{surface}/yy.ww.build Cut web, Android, iOS, gateway from same tag.
Hotfix in validation release/* release/* then main Merge updated release branch back to main.
Incident after go live release/* or tag release/* then main Merge updated release branch back to main.
Most recent release branch main Keep branch during hotfix window, then lock.
Missed train main main Ship next train, not this one.
Revert main main and maybe release/* Prefer small, explicit reverts.

CI policy and branch protections

main is protected and uses a merge queue. Required checks include compile, unit, lint, size budgets, smoke, quick end to end. Codeowners guard shared surfaces and the gateway. The queue gives everyone the same lane to production.

develop branches are protected and require an owner approval. A convergence bot must be green. If drift exceeds one day, block merges until reconciled. That keeps new work aligned without coordination meetings.

release branches are protected, labelled for risk, and require two approvals. Avoid squash merges and keep ancestry visible for audit. Keep release branches open during the hotfix window and lock them when the window closes.

Automation to add: daily convergence from main into each develop branch. A reconciliation check that refuses to lock or close a release branch until all its commits appear on main. Contract tests in CI between gateway and clients. Auto-lock release branches after N days of inactivity, for example 21 days. Do not delete release branches.

Metrics that prove it works

Time to main for feature branches. Develop drift days per vertical or service. Back-merge latency from release branches to main. Revert rate per train. Change-fail rate during ring rollout and time to recover. Bundle size budgets and variance by surface. Contract break count between gateway and clients. Age of gateway compatibility shims. Lead time for first-time contributors.

Publish a small dashboard. Reward boring, repeatable flow.

Anti-patterns and how to correct them

Perma-develop. A develop branch that never merges back becomes a shadow trunk. Set an SLA to close or merge within five working days of the first integration PR or file an exception with a plan.

Release-only fixes. A fix shipped to release without reconciling main will regress next train. Block the release branch from closing until the back-merge into main lands.

Long-running features without flags. If a change spans more than a week, it needs a flag or a develop branch with an exit date.

Creative branch names. Breaks automation and ruins auditability. Enforce the regex and decline the push.

Gateway breaking change without a window. Removing a field before clients move forces emergency releases. Require a published window and link the removal PR to client migrations.

Adoption plan

Week 0. Turn on naming enforcement. Create develop branches for a small set of verticals and services, including develop/gateway. Publish a short contribution guide so new teams can self-serve.

Week 1. Enable the merge queue on main. Add the convergence bot from main to develop. Turn on contract tests between gateway and clients.

Week 2. Cut your first set of release branches for web, Android, iOS, and gateway from the same tag. Validate the reconciliation check that guards main.

Week 3 to 4. Measure time to main, drift days, back-merge latency, contract breaks, and first-time contributor lead time. Share results weekly. Reduce develop usage as teams learn to flag.

Quarter end. Review develop branch inventory. Remove stale sidings. Tighten budgets and checks. Remove old gateway compatibility shims. Update the contribution guide with the best examples from the quarter.

Reference snippets

List all develop branches

git branch -r | grep 'develop/'

Cut a release set

git switch main && git pull
git tag -a v25.40.0 -m "Cut week 40"
git push origin v25.40.0
git switch -c release/web/25.40.0      && git push -u origin HEAD
git switch main && git switch -c release/android/25.40.0 && git push -u origin HEAD
git switch main && git switch -c release/ios/25.40.0     && git push -u origin HEAD
git switch main && git switch -c release/gateway/25.40.0 && git push -u origin HEAD

Daily convergence job, pseudo

for b in $(git branch -r | grep 'develop/' | sed 's|origin/||'); do
  git fetch origin
  git checkout $b
  git rebase origin/main || exit 1
  git push --force-with-lease
done

Appendix, branch-name regex

^((cherry-pick|revert)-[a-zA-Z0-9-]+)
|^(develop\/[a-z-]+)$
|^(release(-candidate)?\/([a-z-]+)\/2[4-9]\.(?:0[1-9]|[1-4]\d|5[0-3])\.(?:\d+|x)$)
|^(f\/[a-zA-Z0-9\/-]{6,35})$

Conclusion

This is not a monolith. It is choreography. One trunk gives a shared reality. Short-lived develop branches let verticals coordinate and still move fast. Per-surface release branches respect different cadences across web, native, and the gateway. Calendar versions and clear names make history legible. Contract tests and a simple merge-back rule keep truth aligned with what shipped. The shape is friendly to contribution. A small team in one office can add a screen, tweak a contract, and ship on the next train without waking anyone else. Fewer surprises, higher pace, steadier quality.