Trunk-Based Development vs Feature-Based Development

A comprehensive comparison guide covering workflows, release mechanisms, conflict management, and framework upgrade strategies.


Table of Contents

  1. Core Philosophies
  2. Head-to-Head Comparison
  3. Pros and Cons
  4. When to Follow Which?
  5. Branching Strategy Walkthroughs
  6. Feature-Based Development Release Mechanism
  7. Bug Fixes for Old Releases
  8. Avoiding Git Conflicts in Feature-Based Development
  9. Merge vs Rebase — When to Use Which at Every Merge Point
  10. Managing Long Framework Version Upgrades
  11. Q&A — Feature-Based Development (GitFlow) Scenarios
    1. What if a feature needs changes right after it was merged into develop?
    2. Where do bug fix branches come from during release hardening?
    3. How are patch fixes / patch releases maintained in Feature-Based Development?
    4. Where is a patch release actually deployed from?
    5. If a release branch passes QA with zero fixes, do we still merge it into develop and main?
    6. What is the difference between release/vX.X and hotfix/vX.X.X (patch) branches?
    7. Why maintain both main and develop if they contain almost the same code?
    8. When to do a Major, Minor, or Patch release?

1. Core Philosophies

Trunk-Based Development (TBD)

  • The Model: Developers merge small, frequent updates to a single “trunk” (main branch) multiple times a day.
  • The Goal: Eliminate long-lived branches to ensure the codebase is always in a “releasable” state.
  • Mechanism: If a feature isn’t ready for users, it is hidden behind Feature Flags (toggles) rather than being kept on a separate branch.

Feature-Based Development (GitFlow)

  • The Model: Developers create dedicated branches for each feature, which may live for days or weeks.
  • The Goal: Isolate work until it is 100% complete and tested before merging it into the main development line.
  • Mechanism: Uses multiple persistent branches (e.g., develop, release, hotfix, feature/xyz).

2. Head-to-Head Comparison

AspectTrunk-Based DevelopmentFeature-Based Development
Branch LifespanHours (max 1–2 days)Days, weeks, or months
Merge FrequencyVery High (Multiple times daily)Low (Only at feature completion)
Merge ConflictsSmall & easy to fixHigh risk of “Merge Hell”
TestingRelies on heavy AutomationRelies on manual/isolation testing
DeploymentSupports Continuous DeploymentSupports Scheduled Releases
VisibilityEveryone sees everyone’s code dailyCode is hidden until the Merge Request

3. Pros and Cons

Trunk-Based Development

Pros

  • Speed: Accelerates the feedback loop; bugs are caught almost immediately after they are written.
  • Simplicity: No complex branching logic or “branch management” overhead.
  • Collaboration: Forces team members to stay in sync, reducing duplicate work.
  • DORA Excellence: A hallmark of elite engineering organizations, as recognized by DORA research.

Cons

  • High Pressure on CI: Requires a bulletproof automated test suite. If the trunk breaks, everyone is blocked.
  • Seniority Required: Can be dangerous for very junior teams who might accidentally “break the build” frequently.
  • Complexity in Code: Requires managing Feature Flags, which can lead to technical debt if not cleaned up.

Feature-Based Development

Pros

  • Isolation: You can experiment or build massive features without affecting the production-ready code.
  • Controlled Reviews: Ideal for regulated industries where every feature needs a formal, exhaustive sign-off.
  • Easier for Juniors: Provides a “safe space” to work without fear of crashing the main site.

Cons

  • Divergence: The longer a branch lives, the harder it is to merge. You often spend more time fixing conflicts than writing code.
  • Slower Delivery: Integration happens at the end, meaning bugs are often discovered weeks after the code was written.
  • Review Bottlenecks: Pull Requests (PRs) become massive, making them exhausting and difficult to review properly.

4. When to Follow Which?

Choose Trunk-Based Development if:

  • You are aiming for Continuous Delivery (CD) and want to deploy several times a day.
  • Your team is experienced and comfortable with automated testing.
  • You are building a SaaS or Web App where there is only one “live” version.
  • You want to improve team collaboration and visibility.

Choose Feature-Based Development if:

  • You are building Open Source software where you can’t trust every contributor’s direct commits.
  • You have Junior-heavy teams who need a sandbox to learn.
  • You must maintain Multiple Versions of the software simultaneously (e.g., supporting v1.0, v2.0, and v3.0 all at once).
  • You have a very strict, manual QA/Release process required by law or contract.

The “Modern Middle Ground”: Scaled Trunk-Based Development

Many high-performing teams use a hybrid approach:

  1. Create a short-lived feature branch.
  2. Work on it for less than 24 hours.
  3. Open a Pull Request, get a quick review, and merge it into the trunk.
  4. If the feature isn’t done, keep it “off” using a feature flag in the code.

5. Branching Strategy Walkthroughs

End-to-end examples using ASCII diagrams for both strategies, covering feature development, release cycles, hotfixes, and legacy version support.


Trunk-Based Development Walkthrough

Step 1 — Feature Work (Short-Lived Branch)

Each developer works on a branch that lives for less than 24 hours. Incomplete features are merged behind a feature flag so they cannot affect users.

gitGraph commit id: "v1.0" branch feat/search commit id: "S1" commit id: "S2" commit id: "S3" checkout main merge feat/search id: "M1" tag: "flag=OFF" branch feat/checkout commit id: "K1" commit id: "K2" commit id: "K3" commit id: "K4" checkout main merge feat/checkout id: "M2" tag: "flag=ON"

Step 2 — Release (Tag on Main, No Dedicated Release Branch)

There is no release branch. The main branch is always deployable. A tag marks a production release, and the CI/CD pipeline handles the rest.

gitGraph commit id: "v1.0" tag: "v1.0" commit id: "v1.1" tag: "v1.1" commit id: "v1.2" tag: "v1.2" commit id: "v1.3" tag: "v1.3" commit id: "v2.0" tag: "v2.0"

CD deploys automatically on every tagged commit.

Step 3 — Hotfix (Fix Committed Directly on Main)

There is no hotfix branch. The developer commits the fix directly to main. The CI/CD pipeline validates and deploys it within minutes.

gitGraph commit id: "..." type: NORMAL commit id: "v2.0" tag: "v2.0" commit id: "bug fix" type: HIGHLIGHT commit id: "v2.0.1" tag: "v2.0.1"

The fix is committed directly to main. CD deploys within minutes.

Step 4 — Old Version Support

TBD deliberately maintains only one live version. If your product requires concurrent support for old releases, TBD is not the right model — use GitFlow with Support Branches.


Feature-Based Development (GitFlow) Walkthrough

Step 1 — Feature Work (Feature Branch → Develop)

Each feature lives on its own branch and only merges to develop after a full code review and approval.

gitGraph commit id: "init" branch develop commit id: "d1" branch feat/auth commit id: "A1" commit id: "A2" commit id: "A3" commit id: "A4" commit id: "A5" checkout develop merge feat/auth id: "PR-auth" branch feat/dashboard commit id: "D1" commit id: "D2" commit id: "D3" commit id: "D4" commit id: "D5" commit id: "D6" checkout develop merge feat/dashboard id: "PR-dash"

Step 2 — Release Cycle (Develop → Release Branch → Main)

Once enough features accumulate in develop, a release branch is cut. Only bug fixes go in. After QA sign-off it merges to main, gets a version tag, and is deployed. It is then synced back to develop.

gitGraph commit id: "v1.0" tag: "v1.0" branch develop commit id: "d1" commit id: "features" branch release/v2.0 commit id: "QA fix 1" commit id: "QA fix 2" commit id: "QA fix 3" checkout develop merge release/v2.0 id: "sync back" checkout main merge release/v2.0 id: "v2.0" tag: "v2.0"

Step 3 — Hotfix (Bug in the Current Live Version on Main)

A hotfix branch is cut from main, not develop. After the fix is verified, it merges into both main (for the immediate release) and develop (to prevent the bug from reappearing in the next version).

gitGraph commit id: "v2.0" tag: "v2.0" branch hotfix/v2.0.1 commit id: "fix 1" commit id: "fix 2" commit id: "fix 3" checkout main merge hotfix/v2.0.1 id: "v2.0.1" tag: "v2.0.1"

The hotfix is also backported into develop to prevent regression:

gitGraph commit id: "d1" commit id: "d2" branch hotfix/v2.0.1 commit id: "backport" checkout main merge hotfix/v2.0.1 id: "sync"

Step 4 — Support Branch (Bug Fix for an Old Version)

If a client reports a critical bug in v1.0 while main is already at v3.0, create a Support Branch from the old git tag. Fixes stay on this branch and are tagged as legacy patch releases. If the bug also affects the current version, use git cherry-pick to apply it to main.

gitGraph commit id: "v1.0.0" tag: "v1.0.0" branch support/v1.x commit id: "fix" commit id: "v1.0.1" tag: "v1.0.1"

If the bug also affects the current version, cherry-pick the fix into main:

gitGraph commit id: "v3.0" tag: "v3.0" commit id: "cherry-pick" type: HIGHLIGHT commit id: "v3.0.1" tag: "v3.0.1"

Full Picture — All Branch Types Across Time

This diagram consolidates every branch type from the GitFlow model into a single timeline. Reading it top-to-bottom shows how the branches interact with each other across a product’s lifecycle:

gitGraph commit id: "v1.0" tag: "v1.0" branch support/v1.x commit id: "v1.0.1" tag: "v1.0.1" checkout main branch develop commit id: "d1" branch feat/auth commit id: "A1" commit id: "A2" commit id: "A3" checkout develop merge feat/auth id: "PR-auth" branch feat/dashboard commit id: "D1" commit id: "D2" commit id: "D3" checkout develop merge feat/dashboard id: "PR-dash" branch release/v2.0 commit id: "QA1" commit id: "QA2" checkout develop merge release/v2.0 id: "sync" checkout main merge release/v2.0 id: "v2.0" tag: "v2.0" branch hotfix/v2.0.1 commit id: "fix" checkout main merge hotfix/v2.0.1 id: "v2.0.1" tag: "v2.0.1" checkout develop merge hotfix/v2.0.1 id: "backport"

Reading the diagram:

  1. Support Branch (top) — A support/v1.x branch was created from the old v1.0 tag long after the team moved on. A patch (v1.0.1) is applied and shipped only to legacy clients still running v1.
  2. Feature Branches → Developfeat/auth is developed independently, then merged into develop via a Pull Request. Later, feat/dashboard follows the same pattern. Each represents a commit on the feature branch.
  3. Release Branch — Once develop has enough features, a release/v2.0 branch is cut. Only QA bug fixes go here (no new features). After hardening, it merges down into main.
  4. Main — Receives the stable release as v2.0. This is the only branch that reaches production.
  5. Hotfix Branch (bottom) — A critical bug is found in v2.0 after it goes live. A hotfix/v2.0.1 branch is cut directly from main, the fix is applied, and it merges back into both main (tagged as v2.0.1) and develop to prevent the bug from reappearing in the next release cycle.

Key takeaway: In GitFlow, code flows in one direction — from feature branches → developreleasemain — with hotfixes being the sole exception that flows the opposite way, from main back into develop.


6. Feature-Based Development Release Mechanism

In Feature-Based Development (GitFlow), the release mechanism is a structured, multi-stage process designed to ensure that only fully tested and approved features reach the end-user. Unlike Trunk-Based Development, where the main branch is always live, this model uses a buffer between development and production.

The Release Workflow

Stage 1: The Integration Stage (develop branch)

As individual feature branches are completed, they are merged into a central develop branch. This branch acts as the “staging area” for the next release. It contains all the features targeted for the upcoming version but is not yet considered stable for production.

Stage 2: The Branching Stage (release/vX.X branch)

When the team decides there are enough features for a release, a Release Branch is created from develop.

  • The Freeze: No new features are allowed to enter this branch.
  • Purpose: This branch is dedicated solely to “polishing” — bug fixes, documentation updates, and minor tweaks.

Stage 3: The Hardening Stage (QA & UAT)

The release branch is deployed to a staging or UAT (User Acceptance Testing) environment.

  • QA engineers perform regression testing.
  • If bugs are found, they are fixed directly on the release branch and then merged back into develop to ensure the next release doesn’t re-introduce the same bug.

Stage 4: The Final Merge (main / master branch)

Once the release branch is stable and signed off:

  • It is merged into the main branch.
  • A Version Tag (e.g., v2.4.0) is applied to the merge commit.
  • The main branch is then deployed to the production environment.

Stage 5: The Sync Stage

Finally, the release branch is merged back into develop one last time. This ensures that any “last-minute” bug fixes made during the hardening stage are reflected in the ongoing development of the next version.

Comparison of Release Mechanics

AspectFeature-Based (GitFlow)Trunk-Based
Release TimingBatched (Monthly, Bi-weekly)Continuous (Daily, Hourly)
GatingPull Requests & Release BranchesAutomated Testing & Feature Flags
StabilityBuilt through a “Hardening” phaseBuilt-in by keeping trunk “Green”
HotfixesRequires a dedicated hotfix branchFixed on trunk and pushed immediately

When This Mechanism Is Most Effective

  • Regulatory Compliance: When you need a “paper trail” of exactly what was tested and approved before it went live.
  • Mobile Apps: Since you cannot “undo” a download once a user has it, the extra hardening phase on a release branch provides a safety net.
  • Complex Migrations: When multiple features depend on a massive database schema change that cannot be easily toggled off.

7. Bug Fixes for Old Releases

Handling a bug in an old version while a newer version is already live requires a specific “Hotfix” or “Support” mechanism. Since your main branch usually points to the latest release, you can’t simply push a fix there without involving the new code.

Approach 1: The “Hotfix” Approach (For the Current Live Version)

If the bug is in the version that was just released and is currently sitting on main, use a Hotfix Branch.

  1. Branch: Create a hotfix/v1.1.1 branch directly from main.
  2. Fix: Apply the code fix and verify it in isolation.
  3. Merge & Tag: Merge the fix into main and create a new tag (e.g., v1.1.1).
  4. Backport: Crucially, you must also merge that hotfix into develop (and any active release branches) so the bug doesn’t reappear in the next release.

Approach 2: The “Support Branch” Approach (For Much Older Versions)

If you have already released v2.0, but a client is still using v1.0 and needs a critical patch, use Support Branches. This is common in Enterprise software or APIs.

  1. Locate the Tag: Find the git tag for the old release (e.g., v1.0.0).
  2. Create a Support Branch: Create a branch from that tag: git checkout -b support/v1.x v1.0.0.
  3. Apply the Patch: Commit the fix to this support branch.
  4. Release & Tag: Deploy from this branch and tag it as v1.0.1.

Note: These changes are rarely merged back into main because the codebases have likely diverged too much. Instead, you manually “cherry-pick” the fix into the current version if the bug still exists there.

Comparison of Fix Strategies

ScenarioStrategyWorkflow
Bug in the latest production versionHotfixBranch from main, merge to main & develop.
Bug in a version 2-3 years oldSupport BranchBranch from the old tag, stay on that branch.
Bug found during testing (not live)BugfixFix directly on the release or feature branch.

Key Maintenance Tips

  • Cherry-Picking: If the bug exists in both the old version and the new version, use git cherry-pick [commit-hash] to apply the exact same fix across different branches without merging unrelated code.
  • Automated Testing: When you fix a bug in an old release, run your full regression suite for that specific version. Old code often lacks the safeguards of your newer architecture.
  • Deprecation Policy: Clearly define how many “old” versions you support. Trying to provide fixes for 10 different old versions simultaneously leads to “Maintenance Exhaustion.”

8. Avoiding Git Conflicts in Feature-Based Development

Git conflicts (or “merge hell”) usually happen because branches live too long and diverge from the main codebase. The longer you wait to merge, the higher the chance someone else has modified the same lines.

Strategy 1: Sync Frequently (The “Pull Constantly” Rule)

The biggest mistake is waiting until a feature is 100% finished to see what others have done.

  • Daily Integration: Every morning, pull the latest changes from the develop (or main) branch into your local feature branch.
  • Command: git pull origin develop
  • Why: This forces you to resolve tiny, 2-line conflicts every day rather than a 500-line conflict at the end of the week.

Strategy 2: Prefer Rebase over Merge for Local Work

When you want to bring updates from the base branch into your feature branch, use rebase.

  • The Logic: Rebase takes your commits and “re-stacks” them on top of the latest changes from the base branch. This keeps the history linear and much easier to read.
  • Command: git pull --rebase origin develop
  • Benefit: It prevents the “messy spiderweb” of merge commits in your history.

Strategy 3: Atomic and Small Features

The size of your feature branch is directly proportional to the pain of the merge.

  • Break it down: Instead of one massive feature/user-authentication branch that takes two weeks, break it into feature/auth-ui, feature/auth-api, and feature/auth-validation.
  • Why: Small branches touch fewer files, significantly reducing the “surface area” for potential conflicts.

Strategy 4: Communication & Task Splitting

Conflicts are often a sign of overlapping responsibilities.

  • File Ownership: If two developers are working on the same Angular component or .NET controller simultaneously, a conflict is guaranteed.
  • Architectural Separation: Use the Single Responsibility Principle. If your logic is separated into small, modular services or partial classes, developers can work on the same “feature” while touching entirely different files.

Strategy 5: Use .gitattributes for Consistent Formatting

Sometimes, “conflicts” aren’t even about code — they are about line endings or auto-formatting (tabs vs. spaces).

  • The Fix: Add a .gitattributes file to your root directory to enforce LF or CRLF endings across the whole team.
  • Pre-commit Hooks: Use tools like Husky or Prettier to format code before it’s committed. This ensures that a “Merge Conflict” isn’t just someone’s IDE re-aligning brackets.

Strategy 6: The “Early PR” Strategy

Open a Draft Pull Request as soon as you start working.

  • Even if the code isn’t finished, a Draft PR allows teammates to see which files you are “occupying.”
  • Most Git platforms (GitHub/GitLab/Azure DevOps) will show a “Conflict Warning” on the PR page in real-time if someone else merges a change that clashes with your work-in-progress.

Summary Checklist

ActionFrequencyPurpose
Pull from DevelopTwice DailyCatch changes early.
RebaseDailyKeep history clean.
CommitsHourlySmall, logical “save points.”
CommunicationConstantAvoid touching the same file as a peer.

9. Merge vs Rebase — When to Use Which at Every Merge Point

Choosing the wrong operation at a merge point is one of the most common causes of avoidable conflicts and messy histories. The rule of thumb is simple: rebase to keep your branch up-to-date; merge to deliver your work into a shared branch.

The Core Principle

OperationWhat It DoesGolden Rule
RebaseReplays your commits on top of the target branch’s latest state. Rewrites your commit history.Use on private/local branches only. Never rebase a branch others are working on.
MergeCreates a new “merge commit” that ties two histories together. Preserves both histories as-is.Use when integrating into a shared/protected branch.

Why this matters for conflicts: Rebase resolves conflicts commit-by-commit (small, isolated chunks), while merge resolves them all at once in a single giant diff. Rebasing frequently keeps conflicts tiny; merging a stale branch makes them huge.


Trunk-Based Development — Merge Points

1. Keeping Your Short-Lived Branch Updated (↓ main → feature)

  • Use: Rebase
  • Command: git pull --rebase origin main
  • Why: Your branch lives for hours, not days. Rebasing replays your few commits on top of the latest main, keeping a linear history and surfacing conflicts one commit at a time.

2. Merging Your Branch into Main (↑ feature → main)

  • Use: Merge (Squash Merge preferred)
  • Command: Handled via the PR — select “Squash and Merge” on GitHub / Azure DevOps.
  • Why: Squash merge collapses your branch into a single commit on main, keeping the trunk history clean and easy to bisect. A regular merge commit is also acceptable if you want to preserve the granular commit history.

3. Hotfix Committed to Main

  • Use: Direct Commit (or fast-forward merge)
  • Why: In TBD, hotfixes are committed straight to main. There is no separate branch to merge, so the question of merge vs rebase does not apply.

Feature-Based Development (GitFlow) — Merge Points

1. Keeping Your Feature Branch Updated (↓ develop → feature)

  • Use: Rebase
  • Command: git pull --rebase origin develop
  • Why: This is the highest-impact habit for reducing conflicts. Rebasing daily re-stacks your work on top of the latest develop, so you resolve small conflicts incrementally instead of facing a wall of them at PR time.

Warning: If multiple developers share a feature branch, use merge instead — rebasing a shared branch rewrites history and will force-push over your teammates’ work.

2. Merging a Feature into Develop (↑ feature → develop)

  • Use: Merge (No Fast-Forward)
  • Command: git merge --no-ff feature/auth
  • Why: The --no-ff flag preserves the branch topology in the history, creating an explicit merge commit that shows “this group of commits was the auth feature.” This is critical for traceability in regulated or audited environments.

3. Cutting a Release Branch from Develop

  • Use: Branch (no merge or rebase)
  • Command: git checkout -b release/v2.0 develop
  • Why: This is a branch creation, not a merge. No operation needed — just cut the branch and freeze features.

4. Bug Fixes on the Release Branch (↓ release → develop)

  • Use: Merge
  • Command: git checkout develop && git merge release/v2.0
  • Why: Bug fixes made during QA hardening must flow back into develop so they are not lost in the next release. A merge commit clearly records the sync point.

5. Release Branch into Main (↑ release → main)

  • Use: Merge (No Fast-Forward)
  • Command: git checkout main && git merge --no-ff release/v2.0
  • Why: This is the production delivery point. The merge commit acts as a permanent record of exactly which release branch produced this version. Tag the resulting commit (v2.0).

6. Hotfix Branch into Main (↑ hotfix → main)

  • Use: Merge (No Fast-Forward)
  • Command: git checkout main && git merge --no-ff hotfix/v2.0.1
  • Why: Same rationale as the release merge — you need a visible record that a hotfix was applied.

7. Hotfix Backport into Develop (↓ hotfix → develop)

  • Use: Merge (or Cherry-Pick)
  • Command: git checkout develop && git merge hotfix/v2.0.1
  • Why: Merge to bring the fix into the development line. If develop has diverged significantly and the merge drags in unwanted changes, use git cherry-pick [commit-hash] to apply only the specific fix commit.

8. Fix for a Legacy Support Branch

  • Use: Cherry-Pick (not merge, not rebase)
  • Command: git checkout support/v1.x && git cherry-pick [commit-hash]
  • Why: The legacy branch and main have diverged so far that a merge would pull in years of unrelated changes. Cherry-pick applies only the exact fix commit, nothing else.

Quick Reference Table

Merge PointDirectionOperationWhy
Update feature from develop/main↓ into your branchRebaseSmall conflicts, linear history
Feature → Develop↑ into shared branchMerge (--no-ff)Preserves branch topology
Feature → Main (TBD)↑ into trunkSquash MergeClean single-commit history
Release → Main↑ into productionMerge (--no-ff)Auditable release record
Release → Develop (sync)↓ backportMergePrevents bug regression
Hotfix → Main↑ into productionMerge (--no-ff)Visible hotfix record
Hotfix → Develop↓ backportMerge / Cherry-PickPrevents bug regression
Fix → Legacy Support Branch↓ isolated patchCherry-PickAvoids dragging in unrelated code

The One Rule to Remember

Rebase down, Merge up. Pull changes down into your private branch with rebase. Push changes up into a shared branch with merge.


10. Managing Long Framework Version Upgrades

Managing a major framework upgrade (like moving from .NET 6 to 8 or Angular 14 to 18) within a Feature-Based Development model is one of the most difficult tasks. If you create a single “Upgrade” branch and work on it for a month, the merge conflict at the end will be catastrophic.

Pattern 1: Branch by Abstraction

Instead of trying to upgrade the whole framework at once, create an abstraction layer that allows the old and new versions to coexist or makes the eventual “swap” a single-line change.

  • The Strategy: Wrap framework-specific features (e.g., HTTP clients, state management, or Auth providers) in your own interfaces or wrapper services.
  • The Benefit: You can upgrade the underlying implementation on your feature branch while keeping the interface the same for the rest of the team.

Pattern 2: The “Bridge” or “Dual-Track” Branching

This is a more structured approach for team environments:

  1. Create a Long-Lived “Stability” Branch: Create a branch called chore/framework-upgrade.
  2. Regular Back-Merging: Every single day, you must merge develop into the upgrade branch.
    • Why? You want to fix the framework-related breaking changes on new feature code as it arrives, rather than facing 1,000 errors on merge day.
  3. The “Big Flip”: Once the upgrade branch is stable, you perform a “Merge Train” where you freeze develop for a few hours, merge the upgrade in, and then everyone else rebases their active features onto the new version.

Modern frameworks often allow for incremental upgrades. For example, in Angular you can use Standalone Components alongside Modules.

  • Step-by-Step: Do not upgrade the whole app. Create a “Migration Epic” and create small feature branches for specific modules:
    • Branch A: Upgrade the Build Pipeline (Webpack to Esbuild/Vite).
    • Branch B: Upgrade the Core Library.
    • Branch C: Migration of Shared UI Components.
  • The Benefit: This allows you to merge parts of the upgrade into the main/develop branch early, reducing the “delta” between the two versions.

Pattern 4: The “Strangler Fig” Pattern

If the framework upgrade is a massive rewrite (e.g., moving from an old monolith to a new architecture), use a proxy.

  • Mechanism: Route a small percentage of traffic (or specific routes) to the “new framework” version of the app while the rest remains on the “old framework” version.
  • Release: Once all features are migrated and tested on the new version, you “strangle” the old version and decommission it.

Comparison of Upgrade Strategies

StrategyConflict RiskSpeedBest For
Big Bang Merge🔴 ExtremeFast (initially)Very small projects.
Back-Merging Daily🟡 MediumModerateStandard enterprise apps.
Incremental (Feature Flags)🟢 LowSlowHigh-traffic, mission-critical apps.

Technical Safety Checklist

  • Dual CI Pipelines: If possible, set up two CI jobs. One that runs the current develop code and one that runs the upgrade branch code.
  • Breaking Change Audit: Before starting, create a document listing every breaking change in the framework and assign “owners” to investigate how they affect your specific codebase.
  • Lock Dependencies: During an upgrade, strictly lock your package.json or NuGet versions to prevent “version drift” where a sub-dependency breaks the build.

11. Q&A — Feature-Based Development (GitFlow) Scenarios

1. What if a feature needs changes right after it was merged into develop?

It depends on when the change is needed and whether a release branch has been cut yet:

Scenario 1: Change needed right after merge, no release branch yet

Create a new feature branch from develop and treat it as a new piece of work:

gitGraph commit id: "d0" branch feat/auth commit id: "A1" commit id: "A2" commit id: "A3" checkout main merge feat/auth id: "PR-auth" branch feat/auth-fix commit id: "F1" commit id: "F2" checkout main merge feat/auth-fix id: "PR-fix"

You do not reopen or continue the old branch. The old merge commit is already part of develop’s history. You branch off develop (which now contains your feature), make the fix, open a new PR, and merge again. This is the standard approach.

Scenario 2: Change needed after a release branch was already cut

The fix goes on the release branch (if the feature is part of that release), then gets synced back to develop:

gitGraph commit id: "v1.0" tag: "v1.0" branch develop commit id: "d1" branch release/v2.0 commit id: "R1" commit id: "R2" commit id: "fix" type: HIGHLIGHT checkout develop merge release/v2.0 id: "sync" checkout main merge release/v2.0 id: "v2.0" tag: "v2.0"

Scenario 3: Change needed after it’s already on main (deployed)

This becomes a hotfix — branch from main, fix, merge back to both main and develop:

gitGraph commit id: "v2.0" tag: "v2.0" branch hotfix/auth-fix commit id: "fix1" commit id: "fix2" checkout main merge hotfix/auth-fix id: "v2.0.1" tag: "v2.0.1"

The hotfix is also backported into develop:

gitGraph commit id: "d1" commit id: "d2" branch hotfix/auth-fix commit id: "backport" checkout main merge hotfix/auth-fix id: "sync"

Key point: In GitFlow, once a feature branch is merged into develop, that branch is considered “done” and typically deleted. Any further changes are just new branches off develop — Git doesn’t distinguish between “fixing an old feature” and “building a new one.” It’s all just commits flowing into develop via PRs.


2. Where do bug fix branches come from during release hardening?

During the QA hardening phase, bug fix branches are created from the release branch itself — not from develop or main. Each developer branches off the release, fixes their assigned bug, and merges back via a PR. Multiple bugfix branches can run in parallel.

gitGraph commit id: "init" branch release/v2.0 branch bugfix/login commit id: "login fix" checkout release/v2.0 merge bugfix/login id: "M1" branch bugfix/cart commit id: "cart fix 1" commit id: "cart fix 2" checkout release/v2.0 merge bugfix/cart id: "M2" commit id: "final" checkout main merge release/v2.0 id: "v2.0" tag: "v2.0"

Bug fixes are also synced back to develop periodically.

Key rules:

  • Branch from: release/v2.0
  • Merge back to: release/v2.0 (via PR, with review)
  • Never branch from develop or main for release bugs — develop may already have new features for v2.1 that shouldn’t be in the v2.0 release. The release branch is frozen to the v2.0 scope.
  • Sync periodically: Merge release/v2.0 into develop so bug fixes don’t get lost.

3. How are patch fixes / patch releases maintained in Feature-Based Development?

A patch release (e.g., v2.0.1, v2.0.2) is a small, targeted fix for a version that is already live in production. In GitFlow the mechanism depends on whether you are patching the current live version or an older one.

Patching the Current Live Version (Hotfix Flow)

If main is at v2.0 and a critical bug is found, the standard hotfix flow applies:

  1. Branch from main: git checkout -b hotfix/v2.0.1 main
  2. Fix, test, and get a review on the hotfix branch.
  3. Merge into main with --no-ff and tag the result as v2.0.1.
  4. Backport into develop (and any active release branch) so the fix is not lost.
gitGraph commit id: "v2.0" tag: "v2.0" branch hotfix/v2.0.1 commit id: "fix1" commit id: "fix2" checkout main merge hotfix/v2.0.1 id: "v2.0.1" tag: "v2.0.1" branch hotfix/v2.0.2 commit id: "fix3" checkout main merge hotfix/v2.0.2 id: "v2.0.2" tag: "v2.0.2"

Each hotfix is also backported into develop:

gitGraph commit id: "d1" branch hotfix/v2.0.1 commit id: "bp1" checkout main merge hotfix/v2.0.1 id: "sync1" branch hotfix/v2.0.2 commit id: "bp2" checkout main merge hotfix/v2.0.2 id: "sync2"

Each hotfix is an independent, short-lived branch. Multiple patch releases can follow one after another — v2.0.1, v2.0.2, etc. — each branching from the latest state of main.

Patching an Older Version (Support Branch Flow)

If main has moved to v3.0 but a client still runs v2.0, you cannot hotfix from main because it contains v3 code. Instead, use a support branch:

  1. Create a support branch (if one doesn’t exist): git checkout -b support/v2.x v2.0.0
  2. Branch from the support branch: git checkout -b patch/v2.0.1 support/v2.x
  3. Fix, test, merge back into support/v2.x, and tag as v2.0.1.
  4. Cherry-pick the fix into main / develop if the bug also exists in the current version.
gitGraph commit id: "v2.0.0" tag: "v2.0.0" branch support/v2.x commit id: "patch 1" commit id: "v2.0.1" tag: "v2.0.1" commit id: "patch 2" commit id: "v2.0.2" tag: "v2.0.2" checkout main commit id: "v3.0" tag: "v3.0" commit id: "cherry-pick" type: HIGHLIGHT commit id: "v3.0.1" tag: "v3.0.1"

The support branch lives indefinitely — as long as you have clients on that major version.

Patch Release Versioning Convention

VersionMeaningSource Branch
v2.0.0Initial releaserelease/v2.0main
v2.0.1First patch (hotfix)hotfix/v2.0.1main (or support/v2.x)
v2.0.2Second patchhotfix/v2.0.2main (or support/v2.x)
v2.1.0Minor release (new features, no breaking changes)release/v2.1main

Key Rules

  • One patch = one hotfix branch. Do not bundle unrelated fixes into the same patch — it defeats the purpose of a small, safe release.
  • Always backport. Every patch merged into main must also reach develop (via merge) and any active release branch (via merge or cherry-pick). Skipping this step is the number-one cause of “bugs that come back.”
  • Tag immediately. After every patch merge into main (or a support branch), tag the commit. The tag is the single source of truth for “what is deployed.”
  • Regression test the patch in isolation. Run your full test suite against the patched branch before merging — not just the test for the fix. Patches applied under pressure are the most likely to introduce new issues.

4. Where is a patch release actually deployed from?

The patch is never deployed from the hotfix branch itself. The hotfix branch is a workspace — once the fix is merged, deployment happens from the target branch (the one that received the merge). Which target branch that is depends on the scenario:

Current Version Patch → Deployed from main

After the hotfix branch merges into main, CI/CD deploys from the tagged commit on main. The sequence is:

  1. hotfix/v2.0.1 merges into main.
  2. Tag v2.0.1 is applied to the merge commit on main.
  3. The CI/CD pipeline triggers on the new tag and deploys from main.
gitGraph commit id: "v2.0" tag: "v2.0" branch hotfix/v2.0.1 commit id: "fix 1" commit id: "fix 2" checkout main merge hotfix/v2.0.1 id: "v2.0.1" tag: "v2.0.1"

The tag on main is what CI/CD uses to trigger deployment. The hotfix branch is deleted after the merge.

Older Version Patch → Deployed from support/vX.x

If the patch targets an old version that is no longer on main, the fix merges into the support branch, and deployment happens from the tagged commit on the support branch:

  1. patch/v2.0.1 merges into support/v2.x.
  2. Tag v2.0.1 is applied to the merge commit on support/v2.x.
  3. The CI/CD pipeline (or a manual deployment) deploys from support/v2.x.
gitGraph commit id: "v2.0" tag: "v2.0" branch support/v2.x branch patch/v2.0.1 commit id: "fix 1" commit id: "fix 2" checkout support/v2.x merge patch/v2.0.1 id: "v2.0.1" tag: "v2.0.1"

The tag on support/v2.x triggers deployment. main (at v3.0) is untouched.

Why not deploy the patch from develop?

develop is a moving target — it contains work-in-progress for the next release. By the time you need a patch for v2.0, develop may already have half-finished features for v2.1 or v3.0. Deploying from develop would ship untested, incomplete code alongside your one-line fix.

gitGraph commit id: "v2.0 merged" commit id: "feat/X" type: REVERSE commit id: "feat/Y" type: REVERSE commit id: "feat/Z" type: REVERSE commit id: "WIP" type: REVERSE

Deploying from develop would ship half-done, untested, unreviewed code just to deliver a single bug fix.

The whole point of a hotfix is surgical precision — you branch from the exact code that is running in production (main or a support branch), change only what is broken, and deploy only that. develop has diverged from production the moment the first new feature was merged after the release.

Summary

ScenarioHotfix merges intoDeployed fromTag applied on
Bug in the current live versionmainmainmain
Bug in an older versionsupport/vX.xsupport/vX.xsupport/vX.x

Key point: The hotfix/patch branch is never the deployment source. It is always the receiving branch (main or a support branch) that gets tagged, and the tag is what the CI/CD pipeline uses to trigger the deployment.


5. If a release branch passes QA with zero fixes, do we still merge it into develop and main?

Yes — both merges are mandatory regardless of whether any bug fixes were made on the release branch.

The merges serve different purposes and skipping either one breaks the workflow:

Why merge into main?

The merge into main is the release itself. main represents “what is in production.” Without this merge, the release never officially ships.

gitGraph commit id: "v1.0" tag: "v1.0" branch develop commit id: "d1" branch release/v2.0 commit id: "clean pass" type: HIGHLIGHT checkout main merge release/v2.0 id: "v2.0" tag: "v2.0"

Why merge back into develop?

Even if zero bug fixes were made, the release branch may still differ from develop because:

  1. Version bumps or changelog updates were committed on the release branch (e.g., updating package.json version, writing release notes).
  2. New feature commits landed on develop after the release branch was cut. The merge back creates a sync point that ties the two histories together.
  3. Future hotfixes will be merged into both main and develop. If the release branch was never merged back, develop’s history diverges from main’s, making those future merges messier.
gitGraph commit id: "d1" branch release/v2.0 commit id: "version bump" checkout main merge release/v2.0 id: "sync"

The Complete Flow (Clean Release, No Fixes)

gitGraph commit id: "v1.0" tag: "v1.0" branch develop commit id: "d1" branch release/v2.0 commit id: "QA pass" type: HIGHLIGHT checkout develop merge release/v2.0 id: "sync back" checkout main merge release/v2.0 id: "v2.0" tag: "v2.0"

Key point: The merge into main delivers the release. The merge back into develop keeps the two histories aligned. Both happen every time — zero fixes or fifty fixes, it makes no difference to the process.


6. What is the difference between release/vX.X and hotfix/vX.X.X (patch) branches?

They look similar — both produce a deployable version — but they serve fundamentally different purposes, originate from different places, and carry different types of changes.

At a Glance

Aspectrelease/v2.0hotfix/v2.0.1
PurposeShip a planned set of new featuresFix a critical bug in production
Branches fromdevelopmain (or a support/vX.x branch)
ContainsMultiple features + QA hardening fixesA single, surgical fix
LifespanDays to weeks (QA/UAT cycle)Hours to a day (emergency turnaround)
New features allowed?No (feature freeze, but all planned features are already in it)No
Bug fixes allowed?Yes — that is its primary activity during QAOnly the one targeted fix
Merges intomain and developmain and develop
Version bumpMinor or Major (v2.0, v3.0)Patch only (v2.0.1, v2.0.2)
TriggerScheduled sprint/release cycleUnplanned — a production incident

Where They Sit in the Timeline

gitGraph commit id: "init" branch develop commit id: "d1" branch release/v2.0 commit id: "QA1" commit id: "QA2" commit id: "QA3" commit id: "QA4" checkout develop merge release/v2.0 id: "sync" checkout main merge release/v2.0 id: "v2.0" tag: "v2.0" branch hotfix/v2.0.1 commit id: "fix1" commit id: "fix2" checkout main merge hotfix/v2.0.1 id: "v2.0.1" tag: "v2.0.1" checkout develop merge hotfix/v2.0.1 id: "backport"

The Key Distinction

  • A release branch is the final gate for a planned delivery. It is cut from develop when the team decides “we have enough features for the next version.” It goes through a full QA hardening phase where multiple bugs may be found and fixed. The result is a new minor or major version.

  • A hotfix/patch branch is an unplanned emergency response. Something broke in production. You branch from main (the exact code running in production), fix only the broken thing, and ship it immediately. There is no feature freeze because there were never any features — just a targeted repair.

Think of it this way: A release branch is a planned departure — you pack your bags, go through security, and board the plane. A hotfix branch is an emergency landing — something went wrong mid-flight, and you fix it as fast as possible with minimal disruption.


7. Why maintain both main and develop if they contain almost the same code?

It is true that right after a release, main and develop are nearly identical — the release branch was just merged into both. But that similarity is temporary. The moment the next sprint begins, the two branches diverge sharply and serve completely different purposes.

The Two Branches Have Different Jobs

Aspectmaindevelop
RepresentsWhat is currently running in productionWhat will ship in the next release
StabilityAlways stable and deployableMay contain half-finished or unstable work
Who deploys from it?CI/CD pipelines deploy to production from mainCI/CD pipelines deploy to staging/dev environments from develop
Who commits to it?Nobody directly — only receives merges from release and hotfix branchesReceives merges from feature branches
Hotfix source?Yes — hotfixes branch from main because it mirrors productionNo — never hotfix from develop

When They Are in Sync vs. When They Diverge

gitGraph commit id: "v2.0" tag: "v2.0" branch develop commit id: "feat/A" commit id: "feat/B" commit id: "feat/C" checkout main commit id: "hotfix" tag: "v2.0.1"

Immediately after the v2.0 release, both branches point to the same commit. But within days:

  • develop has moved ahead with three new feature merges (feat/A, feat/B, feat/C) that are not yet tested or approved for production.
  • main received a hotfix (v2.0.1) that develop may not have yet (until the hotfix is backported).

The branches are already out of sync. If you had only one branch, you would face an impossible choice: either deploy untested features to production, or block all new development until the hotfix is done.

What Would Go Wrong With a Single Branch?

  1. Cannot hotfix safely. If main is also your development branch, it contains work-in-progress code. Deploying a hotfix would ship half-finished features alongside the fix.
  2. Cannot freeze for QA. A release branch is cut from develop and frozen. If develop and main were the same branch, freezing it would block all developers from committing — no new feature work could happen during the entire QA cycle.
  3. No clear “what is in production?” answer. With two branches, you can always git diff main develop to see exactly what has been developed but not yet released. With one branch, you lose that visibility entirely.

The Analogy

Think of main as the published edition of a book — it is the version readers (users) have in their hands. develop is the author’s working manuscript — it contains new chapters, edits, and experiments that are not yet ready for print. Right after publishing, the manuscript and the book are identical. But the author immediately starts writing the next edition, and the two diverge within hours.

You would never hand a reader the working manuscript and call it “the book.” That is why you keep both.

Key point: main and develop look similar only at the moment of a release. Their purpose is fundamentally different — main is a deployment source, develop is an integration target. Merging them into one branch would force you to choose between stability and velocity, and you would lose both.


8. When to do a Major, Minor, or Patch release?

The version number is not arbitrary — it communicates the nature of the change to every consumer of your software. The industry standard is Semantic Versioning (SemVer), expressed as MAJOR.MINOR.PATCH (e.g., v2.4.1).

The Three Version Bumps

Version PartBumped WhenSignal to ConsumersExample
MAJOR (X.0.0)You introduce breaking changes — existing APIs, behaviors, or contracts change in ways that are not backward-compatible”You will need to update your code to work with this version”v1.0.0v2.0.0
MINOR (0.X.0)You add new features that are backward-compatible — existing functionality is untouched”New capabilities are available, but your existing code still works”v2.0.0v2.1.0
PATCH (0.0.X)You fix bugs without adding features or breaking anything”Something was broken, now it’s fixed — safe to update immediately”v2.1.0v2.1.1

How This Maps to GitFlow Branches

Release TypeGitFlow BranchSourceTypical Trigger
Major Releaserelease/v3.0Cut from developBreaking API changes, major rewrites, platform upgrades
Minor Releaserelease/v2.1Cut from developNew features accumulated over a sprint/cycle
Patch Releasehotfix/v2.1.1Cut from main (or support/vX.x)Critical bug found in production

Real-World Examples

Major Release (v1.0.0v2.0.0):

  • Migrating from REST to GraphQL APIs — all client integrations must change.
  • Dropping support for an old authentication method (e.g., removing Basic Auth, requiring OAuth 2.0).
  • Upgrading the underlying framework in a way that changes public interfaces (e.g., .NET 6 → .NET 8 with breaking serialization changes).
  • Restructuring the database schema in a non-backward-compatible way.

Minor Release (v2.0.0v2.1.0):

  • Adding a new “Export to PDF” feature to a dashboard.
  • Introducing a new API endpoint (/api/v2/reports) while keeping all existing endpoints intact.
  • Adding dark mode support to the UI.
  • Performance improvements that do not change any public behavior.

Patch Release (v2.1.0v2.1.1):

  • Fixing a null reference exception that crashes the checkout page.
  • Correcting a calculation error in the tax module.
  • Patching a security vulnerability (e.g., SQL injection in a search endpoint).
  • Fixing a CSS bug where a button is invisible on mobile devices.

Decision Flowchart

flowchart TD A["What changed?"] --> B{"Does it break\nexisting behavior\nor APIs?"} B -- Yes --> C["MAJOR release\n(vX.0.0)"] B -- No --> D{"Does it add\nnew functionality?"} D -- Yes --> E["MINOR release\n(v0.X.0)"] D -- No --> F{"Is it a bug fix\nor security patch?"} F -- Yes --> G["PATCH release\n(v0.0.X)"] F -- No --> H["No version bump needed\n(docs, CI, refactors)"]

Common Mistakes

  • Bumping MAJOR for every release. If nothing is breaking, it should be MINOR. Overusing MAJOR versions erodes consumer trust — teams stop upgrading because every version “might break something.”
  • Shipping new features in a PATCH. A patch should be safe to apply blindly. If it contains new behavior, consumers who auto-update patches may get unexpected changes.
  • Forgetting to bump at all. Deploying changes without updating the version makes it impossible to track which version a client is running or to reproduce bugs.

The Pre-Release Convention

During the QA hardening phase on a release branch, you can use pre-release identifiers to tag intermediate builds:

TagMeaning
v2.1.0-alpha.1Early build, still under active development on the release branch
v2.1.0-beta.1Feature-complete, undergoing QA testing
v2.1.0-rc.1Release Candidate — believed to be final, pending last sign-off
v2.1.0Stable, production-ready release

Key point: The version number is a contract with your consumers. MAJOR means “brace for changes,” MINOR means “new goodies, no risk,” and PATCH means “safe to update right now.” If you follow this discipline, your users can make informed upgrade decisions without reading every changelog.