Trunk-Based Development vs Feature-Based Development
A comprehensive comparison guide covering workflows, release mechanisms, conflict management, and framework upgrade strategies.
Table of Contents
- Core Philosophies
- Head-to-Head Comparison
- Pros and Cons
- When to Follow Which?
- Branching Strategy Walkthroughs
- Feature-Based Development Release Mechanism
- Bug Fixes for Old Releases
- Avoiding Git Conflicts in Feature-Based Development
- Merge vs Rebase — When to Use Which at Every Merge Point
- Managing Long Framework Version Upgrades
- Q&A — Feature-Based Development (GitFlow) Scenarios
- What if a feature needs changes right after it was merged into
develop? - Where do bug fix branches come from during release hardening?
- How are patch fixes / patch releases maintained in Feature-Based Development?
- Where is a patch release actually deployed from?
- If a release branch passes QA with zero fixes, do we still merge it into
developandmain? - What is the difference between
release/vX.Xandhotfix/vX.X.X(patch) branches? - Why maintain both
mainanddevelopif they contain almost the same code? - When to do a Major, Minor, or Patch release?
- What if a feature needs changes right after it was merged into
1. Core Philosophies
Trunk-Based Development (TBD)
- The Model: Developers merge small, frequent updates to a single “trunk” (
mainbranch) 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
| Aspect | Trunk-Based Development | Feature-Based Development |
|---|---|---|
| Branch Lifespan | Hours (max 1–2 days) | Days, weeks, or months |
| Merge Frequency | Very High (Multiple times daily) | Low (Only at feature completion) |
| Merge Conflicts | Small & easy to fix | High risk of “Merge Hell” |
| Testing | Relies on heavy Automation | Relies on manual/isolation testing |
| Deployment | Supports Continuous Deployment | Supports Scheduled Releases |
| Visibility | Everyone sees everyone’s code daily | Code 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:
- Create a short-lived feature branch.
- Work on it for less than 24 hours.
- Open a Pull Request, get a quick review, and merge it into the trunk.
- 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.
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.
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.
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.
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.
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).
The hotfix is also backported into develop to prevent regression:
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.
If the bug also affects the current version, cherry-pick the fix into main:
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:
Reading the diagram:
- Support Branch (top) — A
support/v1.xbranch was created from the oldv1.0tag long after the team moved on. A patch (v1.0.1) is applied and shipped only to legacy clients still running v1. - Feature Branches → Develop —
feat/authis developed independently, then merged intodevelopvia a Pull Request. Later,feat/dashboardfollows the same pattern. Each●represents a commit on the feature branch. - Release Branch — Once
develophas enough features, arelease/v2.0branch is cut. Only QA bug fixes go here (no new features). After hardening, it merges down intomain. - Main — Receives the stable release as
v2.0. This is the only branch that reaches production. - Hotfix Branch (bottom) — A critical bug is found in
v2.0after it goes live. Ahotfix/v2.0.1branch is cut directly frommain, the fix is applied, and it merges back into bothmain(tagged asv2.0.1) anddevelopto prevent the bug from reappearing in the next release cycle.
Key takeaway: In GitFlow, code flows in one direction — from feature branches →
develop→release→main— with hotfixes being the sole exception that flows the opposite way, frommainback intodevelop.
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
developto 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
mainbranch. - A Version Tag (e.g.,
v2.4.0) is applied to the merge commit. - The
mainbranch 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
| Aspect | Feature-Based (GitFlow) | Trunk-Based |
|---|---|---|
| Release Timing | Batched (Monthly, Bi-weekly) | Continuous (Daily, Hourly) |
| Gating | Pull Requests & Release Branches | Automated Testing & Feature Flags |
| Stability | Built through a “Hardening” phase | Built-in by keeping trunk “Green” |
| Hotfixes | Requires a dedicated hotfix branch | Fixed 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.
- Branch: Create a
hotfix/v1.1.1branch directly frommain. - Fix: Apply the code fix and verify it in isolation.
- Merge & Tag: Merge the fix into
mainand create a new tag (e.g.,v1.1.1). - Backport: Crucially, you must also merge that hotfix into
develop(and any activereleasebranches) 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.
- Locate the Tag: Find the git tag for the old release (e.g.,
v1.0.0). - Create a Support Branch: Create a branch from that tag:
git checkout -b support/v1.x v1.0.0. - Apply the Patch: Commit the fix to this support branch.
- Release & Tag: Deploy from this branch and tag it as
v1.0.1.
Note: These changes are rarely merged back into
mainbecause 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
| Scenario | Strategy | Workflow |
|---|---|---|
| Bug in the latest production version | Hotfix | Branch from main, merge to main & develop. |
| Bug in a version 2-3 years old | Support Branch | Branch from the old tag, stay on that branch. |
| Bug found during testing (not live) | Bugfix | Fix 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(ormain) 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-authenticationbranch that takes two weeks, break it intofeature/auth-ui,feature/auth-api, andfeature/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
.gitattributesfile to your root directory to enforceLForCRLFendings 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
| Action | Frequency | Purpose |
|---|---|---|
| Pull from Develop | Twice Daily | Catch changes early. |
| Rebase | Daily | Keep history clean. |
| Commits | Hourly | Small, logical “save points.” |
| Communication | Constant | Avoid 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
| Operation | What It Does | Golden Rule |
|---|---|---|
| Rebase | Replays 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. |
| Merge | Creates 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
mergeinstead — 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-ffflag 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
developso 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
develophas diverged significantly and the merge drags in unwanted changes, usegit 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
mainhave 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 Point | Direction | Operation | Why |
|---|---|---|---|
| Update feature from develop/main | ↓ into your branch | Rebase | Small conflicts, linear history |
| Feature → Develop | ↑ into shared branch | Merge (--no-ff) | Preserves branch topology |
| Feature → Main (TBD) | ↑ into trunk | Squash Merge | Clean single-commit history |
| Release → Main | ↑ into production | Merge (--no-ff) | Auditable release record |
| Release → Develop (sync) | ↓ backport | Merge | Prevents bug regression |
| Hotfix → Main | ↑ into production | Merge (--no-ff) | Visible hotfix record |
| Hotfix → Develop | ↓ backport | Merge / Cherry-Pick | Prevents bug regression |
| Fix → Legacy Support Branch | ↓ isolated patch | Cherry-Pick | Avoids 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:
- Create a Long-Lived “Stability” Branch: Create a branch called
chore/framework-upgrade. - Regular Back-Merging: Every single day, you must merge
developinto theupgradebranch.- 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.
- The “Big Flip”: Once the upgrade branch is stable, you perform a “Merge Train” where you freeze
developfor a few hours, merge the upgrade in, and then everyone else rebases their active features onto the new version.
Pattern 3: Incremental Migration (Recommended for Angular/.NET)
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/developbranch 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
| Strategy | Conflict Risk | Speed | Best For |
|---|---|---|---|
| Big Bang Merge | 🔴 Extreme | Fast (initially) | Very small projects. |
| Back-Merging Daily | 🟡 Medium | Moderate | Standard enterprise apps. |
| Incremental (Feature Flags) | 🟢 Low | Slow | High-traffic, mission-critical apps. |
Technical Safety Checklist
- Dual CI Pipelines: If possible, set up two CI jobs. One that runs the current
developcode and one that runs theupgradebranch 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.jsonorNuGetversions 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:
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:
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:
The hotfix is also backported into develop:
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 offdevelop— Git doesn’t distinguish between “fixing an old feature” and “building a new one.” It’s all just commits flowing intodevelopvia 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.
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
developormainfor release bugs —developmay 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.0intodevelopso 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:
- Branch from
main:git checkout -b hotfix/v2.0.1 main - Fix, test, and get a review on the hotfix branch.
- Merge into
mainwith--no-ffand tag the result asv2.0.1. - Backport into
develop(and any active release branch) so the fix is not lost.
Each hotfix is also backported into develop:
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:
- Create a support branch (if one doesn’t exist):
git checkout -b support/v2.x v2.0.0 - Branch from the support branch:
git checkout -b patch/v2.0.1 support/v2.x - Fix, test, merge back into
support/v2.x, and tag asv2.0.1. - Cherry-pick the fix into
main/developif the bug also exists in the current version.
The support branch lives indefinitely — as long as you have clients on that major version.
Patch Release Versioning Convention
| Version | Meaning | Source Branch |
|---|---|---|
v2.0.0 | Initial release | release/v2.0 → main |
v2.0.1 | First patch (hotfix) | hotfix/v2.0.1 → main (or support/v2.x) |
v2.0.2 | Second patch | hotfix/v2.0.2 → main (or support/v2.x) |
v2.1.0 | Minor release (new features, no breaking changes) | release/v2.1 → main |
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
mainmust also reachdevelop(via merge) and any activereleasebranch (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:
hotfix/v2.0.1merges intomain.- Tag
v2.0.1is applied to the merge commit onmain. - The CI/CD pipeline triggers on the new tag and deploys from
main.
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:
patch/v2.0.1merges intosupport/v2.x.- Tag
v2.0.1is applied to the merge commit onsupport/v2.x. - The CI/CD pipeline (or a manual deployment) deploys from
support/v2.x.
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.
Deploying from
developwould 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
| Scenario | Hotfix merges into | Deployed from | Tag applied on |
|---|---|---|---|
| Bug in the current live version | main | main | main |
| Bug in an older version | support/vX.x | support/vX.x | support/vX.x |
Key point: The hotfix/patch branch is never the deployment source. It is always the receiving branch (
mainor 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.
Why merge back into develop?
Even if zero bug fixes were made, the release branch may still differ from develop because:
- Version bumps or changelog updates were committed on the release branch (e.g., updating
package.jsonversion, writing release notes). - New feature commits landed on
developafter the release branch was cut. The merge back creates a sync point that ties the two histories together. - Future hotfixes will be merged into both
mainanddevelop. If the release branch was never merged back,develop’s history diverges frommain’s, making those future merges messier.
The Complete Flow (Clean Release, No Fixes)
Key point: The merge into
maindelivers the release. The merge back intodevelopkeeps 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
| Aspect | release/v2.0 | hotfix/v2.0.1 |
|---|---|---|
| Purpose | Ship a planned set of new features | Fix a critical bug in production |
| Branches from | develop | main (or a support/vX.x branch) |
| Contains | Multiple features + QA hardening fixes | A single, surgical fix |
| Lifespan | Days 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 QA | Only the one targeted fix |
| Merges into | main and develop | main and develop |
| Version bump | Minor or Major (v2.0, v3.0) | Patch only (v2.0.1, v2.0.2) |
| Trigger | Scheduled sprint/release cycle | Unplanned — a production incident |
Where They Sit in the Timeline
The Key Distinction
-
A release branch is the final gate for a planned delivery. It is cut from
developwhen 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
| Aspect | main | develop |
|---|---|---|
| Represents | What is currently running in production | What will ship in the next release |
| Stability | Always stable and deployable | May contain half-finished or unstable work |
| Who deploys from it? | CI/CD pipelines deploy to production from main | CI/CD pipelines deploy to staging/dev environments from develop |
| Who commits to it? | Nobody directly — only receives merges from release and hotfix branches | Receives merges from feature branches |
| Hotfix source? | Yes — hotfixes branch from main because it mirrors production | No — never hotfix from develop |
When They Are in Sync vs. When They Diverge
Immediately after the v2.0 release, both branches point to the same commit. But within days:
develophas moved ahead with three new feature merges (feat/A,feat/B,feat/C) that are not yet tested or approved for production.mainreceived a hotfix (v2.0.1) thatdevelopmay 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?
- Cannot hotfix safely. If
mainis also your development branch, it contains work-in-progress code. Deploying a hotfix would ship half-finished features alongside the fix. - Cannot freeze for QA. A release branch is cut from
developand frozen. Ifdevelopandmainwere the same branch, freezing it would block all developers from committing — no new feature work could happen during the entire QA cycle. - No clear “what is in production?” answer. With two branches, you can always
git diff main developto 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:
mainanddeveloplook similar only at the moment of a release. Their purpose is fundamentally different —mainis a deployment source,developis 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 Part | Bumped When | Signal to Consumers | Example |
|---|---|---|---|
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.0 → v2.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.0 → v2.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.0 → v2.1.1 |
How This Maps to GitFlow Branches
| Release Type | GitFlow Branch | Source | Typical Trigger |
|---|---|---|---|
| Major Release | release/v3.0 | Cut from develop | Breaking API changes, major rewrites, platform upgrades |
| Minor Release | release/v2.1 | Cut from develop | New features accumulated over a sprint/cycle |
| Patch Release | hotfix/v2.1.1 | Cut from main (or support/vX.x) | Critical bug found in production |
Real-World Examples
Major Release (v1.0.0 → v2.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.0 → v2.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.0 → v2.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
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:
| Tag | Meaning |
|---|---|
v2.1.0-alpha.1 | Early build, still under active development on the release branch |
v2.1.0-beta.1 | Feature-complete, undergoing QA testing |
v2.1.0-rc.1 | Release Candidate — believed to be final, pending last sign-off |
v2.1.0 | Stable, 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.