The branching strategy decision record: why the integration model you chose determines your merge conflict accumulation surface and your incomplete code deployment exposure

Branching strategies are chosen in a founding session that documents the model name and the tooling configuration but not the operational policies the model requires to remain safe over time. Three failure patterns follow from this gap: the GitFlow team whose six-week feature branches accumulate merge conflict debt that costs four engineering days to resolve at integration time (the founding session documented "GitFlow with feature branches" without specifying a maximum branch age, a required rebase cadence, or a merge complexity review before branch creation); the trunk-based team whose developer commits a partially built checkout flow directly to main because "nothing links to it yet" (the founding session documented "trunk-based development, ship small and often" without specifying that any route, API endpoint, or data mutation added to the codebase requires a feature flag before the code is reachable by any application path — including direct URL navigation, prefetch behavior, and API enumeration by external clients); and the release-branch team whose SQL injection hotfix spans three semantically coupled commits that are cherry-picked to the release branch, where one commit does not apply cleanly, the engineer resolves the conflict by hand, and the resolution calls the old unparameterized query function instead of the new sanitized one — re-introducing the vulnerability in the hotfix release that was supposed to close it.

A 22-person SaaS startup adopted GitFlow in month two of development. The team's engineering lead had used GitFlow at his previous company and found it structured and predictable. The model was explained in the engineering wiki: a permanent main branch that contained production-ready code, a permanent develop branch where completed features were integrated before release, and short-lived feature/* branches where individual features were built before being merged to develop. The wiki specified the branch naming convention and the required PR review count (two approvals). It did not specify how long a feature branch was permitted to stay open, whether open branches were required to rebase against develop at any cadence, or what the process was when two feature branches touched many of the same files.

In the startup's eighth month, two major features were in parallel development. The first, feature/user-auth-refactor, was a complete replacement of the authentication system — migrating from session-based authentication to JWT-based authentication to support the mobile client the company was building. It was opened on a Monday and was expected to take six weeks. The second, feature/billing-system, was the first implementation of subscription billing using Stripe — new database tables, new API endpoints, new frontend billing screens, and integration into the existing user account management code. It was opened two weeks after the auth refactor branch and was also expected to take four to six weeks.

Both branches touched many of the same files. The auth refactor modified the user model, the session middleware, every API endpoint handler that previously expected a session cookie and now needed to validate a JWT, and the frontend components that stored and retrieved authentication state. The billing system modified the user model to add subscription fields, the account settings API endpoints that billing UI called, the user provisioning flow that needed to create a Stripe customer on account creation, and the frontend account settings components that now showed billing information. Neither team knew in detail which files the other branch was modifying — they knew at a high level that both features touched user accounts, but the specific file-level overlap was not reviewed before either branch was created.

The auth refactor branch was open for six weeks and two days. The billing branch was open for five weeks. When the auth refactor was finally ready to merge into develop, the merge produced 47 conflict markers across 23 files. The largest conflict was in the user API controller, where the auth refactor had reorganized the handler structure around a new JWT middleware and the billing branch had added six new handler methods to the same controller file, each of which the auth refactor's middleware pattern needed to be applied to. Resolving the conflict in the user API controller alone took half a day — the engineer doing the merge needed to understand both the new JWT middleware pattern (which she had not written) and the new billing handler methods (which she had not written), and compose them correctly without introducing authentication bypass bugs or breaking the billing handlers' expected parameter shapes.

The total merge conflict resolution took four engineering days across two engineers — one from the auth refactor team and one from the billing team, working together to resolve conflicts that neither could resolve alone because the conflicts were at the intersection of both features' designs. Two bugs were introduced during conflict resolution: one was caught by the test suite during the merge PR's CI run and fixed immediately; the other was a subtle authentication bypass in one of the billing API endpoints that passed all tests (the test for that endpoint was written to call the endpoint with a valid JWT, which the pre-merge code also accepted) but failed to reject requests with expired JWTs because the conflict resolution had placed the JWT validation middleware call after the early-return path that served cached billing data. This bug was found three days after the merge by a security-conscious engineer who was reviewing the diff as part of a routine security review, not by the test suite.

The founding session that established "GitFlow with feature branches" documented the branch naming convention, the PR review requirement, and the merge process from feature branches to develop to main. It did not specify a maximum feature branch age (which would have triggered a required merge or a required decomposition of the feature into smaller increments before the six-week mark). It did not specify a daily or weekly rebase requirement (which would have forced the two teams to resolve their conflicts incrementally, one day at a time, rather than all at once at integration time). It did not specify a pre-branch file overlap review (which would have identified, before either branch was opened, that both features planned to modify the user model and the user API controller, and would have triggered a discussion about sequencing the two features or designing an integration boundary between them). The four engineering days of merge conflict resolution, the two bugs introduced during conflict resolution, and the security review finding were the operational cost of an integration cadence policy that was absent from the branching strategy decision.

A 30-person product SaaS adopted trunk-based development in year two of their product's life, after a painful six-month period with a GitFlow-derived branching model that had produced exactly the kind of long-lived feature branch conflict the model generates when features take longer than expected. The new approach was stated clearly in the engineering wiki: "We use trunk-based development. All changes go to main. Ship small, ship often, keep main always deployable." The team understood the model's core principle: small, frequent commits to a shared main branch reduced integration risk by keeping divergence windows short. Feature flags were mentioned in the wiki as "available via our flag service" but were not specified as a required mechanism for any particular category of change.

Eight months after adopting trunk-based development, a senior engineer named Marcus was building a redesigned multi-step checkout flow. The new checkout was a significant change — it replaced a single-page checkout form with a four-step wizard (cart review → shipping → payment → confirmation), required new frontend routes (/checkout/shipping, /checkout/payment, /checkout/confirm), new API endpoints (POST /api/checkout/shipping, POST /api/checkout/payment), and new database tables for staging checkout state between steps. The work was estimated at three weeks.

Marcus was committed to the trunk-based principle of shipping small. He merged his changes to main frequently — almost daily. At the end of the first week, he had merged the four new frontend route components (each rendering a placeholder "Under construction" UI), the route registration that mapped the URL paths to the components, the three new API endpoint stubs (returning static 200 responses without processing any data), and the database migration creating the checkout_sessions table. None of the routes were linked from the navigation menu or from the existing checkout button. The existing checkout flow was untouched and still worked. From Marcus's perspective, the new code was inert — unreachable because nothing linked to it.

The application's frontend framework was Next.js, which registered all routes defined in the /pages/ directory as navigable URLs automatically. The new checkout route files (/pages/checkout/shipping.tsx, /pages/checkout/payment.tsx, /pages/checkout/confirm.tsx) were immediately navigable via direct URL as soon as they were deployed. The application also included a prefetch library that parsed the site's sitemap on load and preloaded pages that users were statistically likely to visit — and the sitemap had been updated to include the new checkout URLs as part of Marcus's first week merge (he had added them to the sitemap to make sure they would eventually be indexed, not realizing the prefetch library would load them immediately).

Three weeks after Marcus's first merge, a user named Elena was completing a purchase. She was using a browser extension that automatically saved form state across page reloads. After filling in her cart and clicking the existing checkout button, she was directed to the existing single-page checkout form. Her browser extension, which had previously saved her shipping information on a different site that used the path /checkout/shipping, detected the pattern and offered to restore her saved data. She clicked "Restore" — the browser extension navigated to /checkout/shipping, which was now a live route on the company's domain, and populated the form fields with her shipping data from the other site. The page she landed on was Marcus's placeholder component, which rendered a blank white page with a "loading…" spinner (the placeholder UI Marcus had committed in week one before building the actual step content). Elena saw the blank spinner page and assumed the checkout was broken. She filed a support ticket: "Your checkout page is completely broken, just a spinning loader."

The support engineer investigated and discovered that /checkout/shipping was a live, unprotected route serving Marcus's week-one placeholder component. A broader audit of the new checkout routes revealed that all four routes were live and navigable, the three new API endpoints were reachable from any API client, and the checkout_sessions table was in production with no data access controls yet implemented (the row-level security rules Marcus planned to add in week two were not yet written). The audit also found that the /checkout/payment route had been indexed by a search engine crawler that had followed a link from the sitemap, and that the crawler had submitted a GET request to POST /api/checkout/payment using the URL directly (which returned a 405 but logged the attempt). Over the three weeks since Marcus's first merge, 340 users had visited one of the new checkout routes via direct URL — most from browser history (users whose browsers had pre-cached the URL from the prefetch library) or from the search engine's index (which showed the route as a result for searches for the company's name combined with "checkout").

The founding session that established trunk-based development documented the model and the principle. It did not specify that feature flags are required for any incomplete code that adds navigable routes, API endpoints, background jobs, or data mutations. It did not specify that "unreachable" means protected by a feature flag that prevents any code path from reaching the incomplete code — not merely absent from the navigation menu. It did not specify that the sitemap must not include routes for features under active development. And it did not specify that the frontend framework's automatic route registration from the /pages/ directory meant that any file added to that directory was immediately navigable by direct URL. Marcus was a competent engineer following the trunk-based principle correctly as he understood it. The gap was in the policy documentation, not in Marcus's judgment.

A 45-person B2B SaaS company used a release branch model: features were developed on short-lived branches off of main, merged to main after review, and deployed to production via a release/vN.x branch that was cut from main at the start of each release cycle. The release branch received only cherry-picked fixes during its lifetime. New features stayed on main and shipped in the next release cycle. Hotfixes for production issues were committed to main first, then cherry-picked to the current release branch, and deployed as a point release (e.g., v2.4.1).

In the company's third year, a security engineer conducting an internal code review discovered a SQL injection vulnerability in the report generation endpoint. The endpoint accepted a report_type parameter from the client and used it directly in a string-interpolated SQL query: f"SELECT * FROM reports WHERE type = '{report_type}'". A malicious client could submit ' OR '1'='1 as the report_type and receive all reports across all tenant accounts, or submit a more complex payload to exfiltrate the entire database. The vulnerability had been in production for fourteen months.

The security engineer fixed the vulnerability across three commits. The first commit added a parameterized query helper function to the shared database utilities module: db_utils.py. The second commit modified the report generation controller to import the new helper and replace the string-interpolated query with the parameterized version. The third commit added a regression test that submitted a SQL injection payload to the report generation endpoint and asserted that the response was a 400 error with an "invalid report_type" message, not a 200 response containing data. All three commits were merged to main and deployed to production as a hotfix by the end of the day.

The current production release was v2.4, cut from main three weeks earlier. The release manager, Priya, needed to deploy the SQL injection fix as v2.4.1 to all customers running the v2.4 codebase (the company offered an on-premises deployment option, and several enterprise customers were running v2.4 on their own infrastructure; the cloud-hosted version had already received the fix through the direct-to-main deployment). Priya ran git cherry-pick for each of the three commits in order onto the release/v2.4 branch.

The first cherry-pick (the parameterized query helper added to db_utils.py) applied cleanly. The second cherry-pick (the controller modification) did not apply cleanly. In the three weeks since the release/v2.4 branch was cut, a separate PR had been merged to main that reorganized the import structure of the report generation controller — grouping all database utility imports at the top of the file in a standardized block, and renaming the import alias. The cherry-pick conflict was in the import section: the hotfix commit's import of the new helper function conflicted with the reorganization commit's rearrangement of the import block. Priya resolved the conflict by accepting the reorganized import block from the release branch and manually adding the new helper function import, as best she could read the diff. Under time pressure (the fix had already been deployed to cloud-hosted customers and the enterprise customers were waiting for the v2.4.1 package), she completed the resolution, confirmed the file still compiled, and moved on.

The resolution contained a subtle error. The reorganization PR had renamed the database utility module's import alias from db to database. Priya's conflict resolution added the import for the new parameterized query helper using the new alias (from db_utils import parameterized_query), which was correct. But in the function body, where the old string-interpolated query had been replaced with a call to the helper, Priya had looked at the hotfix commit's diff, which showed the call as db.parameterized_query(report_type) — using the old alias db. She copied this call into the release branch controller without noticing that the release branch used the alias database. The code she committed to release/v2.4 read: db.parameterized_query(report_type) — but db was not defined in the release branch's import scope. The file would raise a NameError at runtime when the report generation endpoint was called.

Rather than fixing the undefined name error by using the correct alias, Priya took the path of least resistance to make the code compile: she looked at what db had referred to in the old import structure, found that the original code had used import db_utils as db (the pre-reorganization alias), and added that import back. The file now compiled cleanly: it imported both from db_utils import parameterized_query (unused) and import db_utils as db (used in the function call db.parameterized_query(report_type)). The function call was correct — db_utils.parameterized_query was the right function. Priya ran the release branch's test suite: it passed. The third cherry-pick (the regression test) applied cleanly. Priya tagged release/v2.4.1, built the enterprise deployment package, and distributed it to the five enterprise customers running v2.4.

Three weeks later, a penetration testing firm conducting a security assessment for one of the enterprise customers tested the v2.4.1 deployment. The tester submitted a SQL injection payload to the report generation endpoint: report_type=' UNION SELECT username, password_hash, null FROM users --. The endpoint returned a 200 response containing the customer's user table. The penetration tester reported a critical finding: the SQL injection vulnerability was present in v2.4.1.

The post-mortem reconstructed what had happened. Priya's conflict resolution had correctly called db.parameterized_query(report_type). But the import db_utils as db import she had added to resolve the NameError imported the old module-level parameterized_query function — which had been added to db_utils.py in the first cherry-pick commit. The first cherry-pick had correctly added the new parameterized query function to the module. So the function call was calling the correct function from the correct module. The fix had been applied correctly in the sense that the parameterized query helper was being called.

But the test suite revealed the actual error. The regression test from the third cherry-pick tested the cloud-hosted v2.5 release branch behavior, where the fix had been validated. On the v2.4.1 release branch, an enterprise customer had added a custom middleware that preprocessed the report_type parameter before it reached the controller — stripping special characters as a content-filtering rule the customer had requested eighteen months ago. The middleware stripped the SQL injection characters from the parameter before the controller received it, which meant the parameterized query function never received an injection payload in any of the tests. The enterprise customer's middleware masked the vulnerability in test environments while also masking it in the penetration test for the other enterprise customers who had the same middleware. The enterprise customer who found the vulnerability had disabled the custom middleware three weeks earlier as part of a middleware refactor — and their refactored deployment did not include the character-stripping rule, exposing the underlying vulnerability.

The founding session that established the release branch model and the hotfix cherry-pick process documented the branch naming convention, the cherry-pick workflow, and the release tagging process. It did not specify a cherry-pick verification protocol that cross-referenced every cherry-picked commit hash against the complete set of commits in the original fix before the release was tagged. It did not specify that the full test suite — including security regression tests added as part of the fix — must be run on the release branch after cherry-picking in an environment identical to each enterprise customer's deployment configuration. And it did not specify that conflict resolutions during cherry-picks must be reviewed by the engineer who wrote the original fix, not resolved unilaterally by the release manager under time pressure.

Structural properties set by the branching strategy decision

Three structural properties are determined when a team chooses a branching model. None appear explicitly in the session that selects GitFlow, trunk-based development, or a release branch model — they are operational constraints that emerge from the model's assumptions about integration cadence, code completeness, and patch atomicity.

Property 1: The integration cadence and the merge conflict accumulation surface. The merge conflict accumulation surface is the set of files in any open branch that are also being modified by other concurrent branches. The surface grows as a function of branch age multiplied by codebase change rate. A branch open for one day in an active twelve-engineer codebase will diverge by five to fifteen commits. A branch open for six weeks will diverge by 300 to 500 commits. The key property of merge conflict accumulation is that it is invisible during the branch's lifetime and becomes visible only at integration time — which means that the cost is paid all at once, after the full development cost of the feature has already been incurred, when the team is least positioned to absorb additional engineering time. The only reliable mechanism for controlling merge conflict accumulation is integration frequency: branches that integrate with the main line daily or every two days never accumulate more than a day or two of divergence, and that divergence is resolved incrementally in small pieces rather than in one large conflict resolution event. The policy controls that enforce integration frequency are a maximum branch age (the point at which a branch that has not been integrated must be either merged in its current state behind a feature flag, decomposed into smaller increments that can each be integrated, or escalated for a project timeline discussion), a required rebase cadence for branches that must remain open longer than one day (daily rebase against main keeps the branch's conflict surface bounded to one day's worth of changes), and a pre-branch file overlap review (before a branch is created, the engineer identifies which files the feature will touch, checks whether other open branches touch the same files, and designs integration boundaries if overlap exists). None of these policy controls are inherent to any particular branching model — they are additional commitments that must be documented alongside the model name to be effective. A branching strategy ADR that says "we use GitFlow" without specifying integration cadence commitments has not made a complete branching strategy decision. The feature flag decision record documents the flag infrastructure that enables trunk-based development teams to merge incomplete code to main behind flags — the prerequisite that makes the trunk-based integration cadence sustainable without shipping incomplete features to users.

Property 2: The trunk-based deployment surface and the incomplete code exposure. Trunk-based development's central claim is that deploying small, frequent changes to the main branch reduces integration risk by keeping divergence windows small. This claim is true — but it is conditional on the assumption that every change merged to main is complete and safe to deploy to all users. The incomplete code exposure surface is the set of all routes, API endpoints, background jobs, and data mutations that exist in the deployed codebase but are not yet complete and not yet intended for user access. The surface is non-zero whenever a developer merges code that adds navigable routes, reachable API endpoints, or data-mutating jobs without protecting them with a feature flag that prevents any code path from reaching the incomplete code. "Unreachable" is a claim about the current state of the application's navigation links and documented API surface — it is not a claim about the set of all code paths that the runtime will exercise. Frontend frameworks register routes automatically from filesystem paths. Prefetch and preload behaviors execute before users explicitly navigate. API clients probe URL patterns programmatically. Search engine crawlers follow URLs found in sitemaps regardless of whether those URLs are linked from navigation. Browser extensions restore saved form data to matching URLs. The incomplete code exposure surface closes only when the feature flag prevents any code path from reaching the incomplete code — not when the navigation menu doesn't link to it, not when the route is not yet in the sitemap, and not when the API endpoint is not yet documented. The practical implication is that the feature flag requirement must be specified at the branching strategy level, not left as an optional best practice. A developer committing partial work to main who is not required to use a feature flag will apply their judgment about whether the code is "reachable enough" to need one — and that judgment will be informed by their knowledge of the explicit navigation paths, not by the full set of code paths the runtime exercises. The flag service infrastructure decision record documents the flag evaluation model — the SDK, the evaluation context, and the default behavior when the flag service is unavailable — which determines whether a flag that defaults to disabled in production will correctly prevent access to incomplete code even when the flag service experiences a brief outage.

Property 3: The release branch model and the cherry-pick completeness surface. A release branch model requires a process for applying hotfixes to production code that is behind the main branch's development tip. The cherry-pick is the standard mechanism: identify the fix commits on main, apply them to the release branch, tag a point release. The cherry-pick completeness surface is the set of commits that, if applied without all other semantically coupled commits from the same fix, produce a state that compiles and passes tests but is logically incomplete or incorrect. A fix that spans three commits — a helper function, the caller of that helper, and a regression test — has a completeness surface with six states: all three applied (correct), only the helper (correct but untested and unused), only the caller (fails because helper does not exist), helper and caller (correct but untested), only the test (passes because the test validates behavior that existed before the fix), and caller and test (fails when the fix is correct but the test catches a different caller path that still uses the old code). The cherry-pick process as typically documented specifies which commits to pick but not how to verify that the applied state is semantically equivalent to the fix's state on main. Conflict resolution during cherry-picking introduces additional states — the conflict is resolved correctly, the conflict is resolved incorrectly in a way that produces the intended behavior, and the conflict is resolved incorrectly in a way that appears to work but does not produce the intended behavior. The third case — a resolution that compiles, passes tests, and deploys successfully but does not apply the fix — is the most dangerous because it produces a false signal that the fix was applied correctly. The structural requirement is a cherry-pick verification protocol that closes the completeness surface: before the release branch is tagged, cross-reference every cherry-picked commit hash against the complete list of commits in the original fix (obtained from the PR or issue tracker, not reconstructed from memory); run git diff between the release branch and the original fix's final state for each modified file to verify that the changes match; run the full test suite including any security regression tests added as part of the fix; and require sign-off from the engineer who wrote the original fix, not only the release manager who performed the cherry-pick. The CI/CD pipeline decision record documents the test suite composition — which tests run on the release branch and which run only on main — and is the reference for determining whether the regression test added as part of a fix is included in the release branch's CI run. The deployment strategy decision record documents the artifact promotion model — whether the same build artifact that passed CI on the release branch is deployed to production, or whether a separate build is created for deployment — which determines whether a fix that passes CI on the release branch is guaranteed to be present in the deployed artifact.

What the founding session records and what it omits

The founding branching strategy session — or more typically, the conversation that happened when the engineering lead set up the first repository and wrote the CONTRIBUTING.md — records the model name, the branch naming convention, and the tooling configuration (protected branches, required approvals, CI status checks). What it does not record is the set of operational policies that the model requires to function as intended: the maximum branch age for GitFlow, the feature flag requirement for trunk-based development, the cherry-pick verification protocol for release branches.

The omission is predictable. At the time the branching strategy is chosen, the team is small, features are simple, and the model's failure modes are theoretical rather than experienced. GitFlow's merge conflict accumulation surface is not visible when the team has four engineers and each feature takes one week. The trunk-based incomplete code exposure surface is not visible when the team has eight engineers and every developer knows what every other developer is working on. The release branch cherry-pick completeness surface is not visible when hotfixes are rare and the engineer who wrote the fix is also the engineer who applies it to the release branch. The failure modes become visible at scale — when the team has grown to twenty or thirty engineers, features take six weeks instead of one week, and the engineer who applies the hotfix to the release branch is not the engineer who wrote it.

The branching strategy ADR that is written at founding and never updated is a common pattern. The CONTRIBUTING.md explains how to create a branch and how to submit a PR. It does not explain the maximum branch age or the daily rebase requirement, because those constraints were not relevant when the document was written. By the time those constraints become relevant — when the first large merge conflict incident occurs, or when the first incomplete feature reaches production — the CONTRIBUTING.md's silence on the policy has been read by every engineer on the team as confirmation that no policy exists. Reconstructing the intent of the original branching strategy decision from the code history and the CONTRIBUTING.md at the time of an incident is exactly the kind of archaeology that WhyChose's decision extractor was built to short-circuit: the original AI session that chose the branching model almost certainly discussed the failure modes and the safeguards — branch aging, feature flag requirements, cherry-pick verification — and those safeguards did not make it into the CONTRIBUTING.md because the session was not structured to capture them as commitments.

The branching strategy decision record closes that gap. It documents not just the model name but the integration cadence policy, the feature flag requirement for incomplete code, and the cherry-pick verification protocol for release branches — the three policy commitments that determine whether the model's failure modes are controlled or left to accumulate invisibly until a conflict incident, a support ticket about a broken route, or a penetration test finding makes them visible. The decisions never written down are rarely the high-level choices that everyone remembers — "we use GitFlow" appears in every CONTRIBUTING.md. They are the operational policies that were discussed and agreed in the AI session that chose the model and then never formally recorded: the maximum branch age, the feature flag requirement, the cherry-pick verification step. Those are the decisions that accumulate interest.

The branching strategy ADR: five sections

Section 1: Branching model selection and integration cadence commitment. Specify the selected model (trunk-based development, GitHub Flow, GitFlow, release branch model, or a hybrid), the rationale for selecting it at the current team size and release cadence, and the integration cadence commitment: the maximum age a branch is permitted to stay open before it must be integrated, decomposed into smaller increments, or escalated. For trunk-based development: no branch stays open longer than one to two days; engineers are expected to commit to main at least once per day; PRs should represent increments of one to three hours of work, not complete features. For GitFlow or GitHub Flow with longer-lived branches: the maximum branch age before a required rebase, the required rebase cadence for branches that must remain open longer than two days, and the process for identifying file-level overlap between concurrent branches before they are created. The integration cadence commitment is the primary control on merge conflict accumulation and must be a concrete policy, not a principle ("ship small").

Section 2: Feature flag requirement and incomplete code deployment policy. For trunk-based development teams: specify which categories of change require a feature flag before being merged to main. The requirement applies at minimum to: any new frontend route component added to the framework's automatic route registration directory; any new API endpoint that is not a complete, production-ready implementation; any new background job or scheduled task; and any database migration that modifies a table currently in production use without a corresponding application-level rollback path. The feature flag requirement is not a suggestion — it is a merge prerequisite for any PR that adds code in these categories without a flag already in place. Specify the flag service to be used, the default behavior when the flag service is unavailable (fail closed, meaning the feature is disabled when the flag service cannot be reached, to ensure that incomplete features do not accidentally become visible due to a flag service outage), and the flag removal process (the flag may be removed only after the feature has been enabled for a population of real users without critical issues for a specified minimum period).

Section 3: Release branch model and hotfix cherry-pick verification protocol. For teams using release branches: specify the cherry-pick verification protocol that applies to every hotfix cherry-picked from main to a release branch. The protocol must include: obtaining the complete list of commits in the original fix from the PR or issue tracker before beginning the cherry-pick (not reconstructing the list from git log after the fact); performing the cherry-pick in commit order (earliest to latest) and recording each commit hash as it is applied; after completing all cherry-picks, running git diff between the release branch and the original fix's final commit for each file touched by the fix to verify that the changes are semantically equivalent; running the full test suite on the release branch after cherry-picking, including any regression tests added as part of the fix; and requiring sign-off from the engineer who wrote the original fix before the release branch is tagged for deployment. If the engineer who wrote the fix is unavailable, the sign-off requirement falls to the engineering lead, who must review the diff themselves rather than relying on the release manager's judgment.

Section 4: Branch protection and merge gate configuration. Specify the branch protection rules applied to the main branch and any release branches: required approval count, required CI status checks (which test suites, which linters, which security scans), and whether force pushes to protected branches are permitted (they should not be — a force push to main rewrites history in a way that is visible to all engineers who have pulled from main and produces a diverged state that requires manual recovery). Specify the merge strategy (squash-and-merge, merge commit, rebase-and-merge) and the rationale — squash-and-merge produces a clean linear history that simplifies cherry-picking but loses the individual commit granularity that makes cherry-pick atomicity analysis possible; rebase-and-merge preserves commit granularity but produces a linear history that looks identical to squash-and-merge at the branch level; merge commits preserve the full branch topology at the cost of a less linear history. The cherry-pick verification protocol must account for whichever merge strategy is used, because squash-and-merge changes the commit hash of every commit in the merged PR, which means cherry-picking the squash commit rather than the individual commits.

Section 5: Branching model review cadence and restructuring criteria. Specify the cadence at which the branching strategy is reviewed — the model chosen at eight engineers is not necessarily the right model at thirty engineers, and the migration from one model to another is a significant engineering investment that requires planning. The review should be triggered by any of these signals: merge conflict resolution consuming more than one engineering day per sprint for two consecutive sprints; a deploy frequency that has dropped below one deploy per week for reasons attributable to the branching model rather than to feature scope; a branch staying open longer than twice the maximum branch age specified in Section 1 without escalation; or the team growing past a headcount threshold at which the integration cadence policy has not been re-evaluated. The review should produce a written update to the branching strategy ADR with the rationale for any policy changes, the evidence that triggered the review, and the target state. The new CTO onboarding problem is most acute when the branching strategy ADR was never written — when the incoming technical leader finds a CONTRIBUTING.md that says "use feature branches and submit PRs for review" and cannot determine whether that means GitFlow, GitHub Flow, trunk-based with two-day branches, or something else that evolved organically. The ADR makes the model explicit and reviewable by anyone who joins the team after the founding decision was made. The ADR lifecycle applies here as it does to all architectural decisions: when the branching model changes, the existing ADR is marked superseded and a new ADR is written with the current model, the rationale for the change, and the evidence that drove it — so that three years from now, the engineer asking "why did we switch from GitFlow to trunk-based in year two?" can find the answer without an AI-assisted archaeology session through the git history.

FAQ

How long should a feature branch stay open before it accumulates too much merge conflict risk?

The safe maximum branch age depends on the team's codebase change rate. In a twelve-engineer team where each engineer merges one to two PRs per day, a branch open for six weeks will diverge from main by 500 to 1,000 commits. The practical guideline for most teams: branches open longer than one sprint (two weeks) are high-risk for complex merge conflicts; branches open longer than four weeks should be treated as a branching model failure, not a normal occurrence. The remediation is to change the development model — break the feature into smaller increments integrated behind feature flags, enforce a daily rebase requirement, and identify file-level overlap between concurrent branches before they are created.

What does 'feature flag required for trunk-based development' mean in practice?

It means that any code merged to main that adds a route, endpoint, background job, or data mutation that is not yet complete and production-ready must be wrapped in a feature flag check that defaults to disabled. The flag prevents the incomplete code from being reachable by any code path — not just from visible navigation links, but from direct URL access, prefetch behavior, API enumeration, and redirect chains. "Not linked from anywhere" is not equivalent to "protected by a feature flag." The practical implementation: create the flag before writing any code for the feature, wrap the route registration or endpoint handler in a flag check on the first commit, merge the flag-protected stub to main, then build the feature incrementally behind the flag.

How do you verify that a security hotfix has been completely cherry-picked to a release branch?

Three steps: (1) obtain the complete list of commit hashes in the original fix from the PR or issue tracker before beginning the cherry-pick — not reconstructed from git log afterward; (2) after cherry-picking all commits in order, run git diff between the release branch and the original fix's final commit for each modified file to verify the changes are semantically equivalent; (3) run the full test suite including any regression tests added as part of the fix on the release branch, in an environment that matches the deployment target. Require sign-off from the engineer who wrote the fix before tagging the release branch. If conflict resolution was required during the cherry-pick, the engineer who wrote the fix must review the resolution, not only the release manager who performed it.

When should a team switch from GitFlow to trunk-based development?

Evaluate switching when any of these signals appear: merge conflict resolution consuming more than one engineering day per sprint; features delayed because they cannot be merged until a parallel feature completes; time between "code complete" and merged-to-main exceeding one week; or CI producing different results on feature branches than on main because branches have diverged significantly. The switch requires two preconditions: feature flag infrastructure that allows incomplete code to be merged to main without being reachable in production, and a CI pipeline fast enough that developers can merge multiple times per day without waiting more than fifteen minutes for results. Without feature flags, trunk-based development forces teams to either merge only complete features (reproducing GitFlow's long-branch problem) or ship incomplete features to users.