develop?develop and main?release/vX.X and hotfix/vX.X.X (patch) branches?main and develop if they contain almost the same code?main branch) multiple times a day.develop, release, hotfix, feature/xyz).| 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 |
Many high-performing teams use a hybrid approach:
End-to-end examples using ASCII diagrams for both strategies, covering feature development, release cycles, hotfixes, and legacy version support.
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.
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.
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.
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.
Each feature lives on its own branch and only merges to develop after a full code review and approval.
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.
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:
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:
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/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.feat/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.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.v2.0. This is the only branch that reaches production.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 →
develop→release→main— with hotfixes being the sole exception that flows the opposite way, frommainback intodevelop.
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.
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.
release/vX.X branch)When the team decides there are enough features for a release, a Release Branch is created from develop.
The release branch is deployed to a staging or UAT (User Acceptance Testing) environment.
develop to ensure the next release doesn’t re-introduce the same bug.main / master branch)Once the release branch is stable and signed off:
main branch.v2.4.0) is applied to the merge commit.main branch is then deployed to the production environment.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.
| 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 |
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.
If the bug is in the version that was just released and is currently sitting on main, use a Hotfix Branch.
hotfix/v1.1.1 branch directly from main.main and create a new tag (e.g., v1.1.1).develop (and any active release branches) so the bug doesn’t reappear in the next release.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.
v1.0.0).git checkout -b support/v1.x v1.0.0.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.
| 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. |
git cherry-pick [commit-hash] to apply the exact same fix across different branches without merging unrelated code.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.
The biggest mistake is waiting until a feature is 100% finished to see what others have done.
develop (or main) branch into your local feature branch.git pull origin developWhen you want to bring updates from the base branch into your feature branch, use rebase.
git pull --rebase origin developThe size of your feature branch is directly proportional to the pain of the merge.
feature/user-authentication branch that takes two weeks, break it into feature/auth-ui, feature/auth-api, and feature/auth-validation.Conflicts are often a sign of overlapping responsibilities.
.gitattributes for Consistent FormattingSometimes, “conflicts” aren’t even about code — they are about line endings or auto-formatting (tabs vs. spaces).
.gitattributes file to your root directory to enforce LF or CRLF endings across the whole team.Open a Draft Pull Request as soon as you start working.
| 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. |
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.
| 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.
git pull --rebase origin mainmain, keeping a linear history and surfacing conflicts one commit at a time.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.main. There is no separate branch to merge, so the question of merge vs rebase does not apply.git pull --rebase origin developdevelop, 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.
git merge --no-ff feature/auth--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.git checkout -b release/v2.0 developgit checkout develop && git merge release/v2.0develop so they are not lost in the next release. A merge commit clearly records the sync point.git checkout main && git merge --no-ff release/v2.0v2.0).git checkout main && git merge --no-ff hotfix/v2.0.1git checkout develop && git merge hotfix/v2.0.1develop has diverged significantly and the merge drags in unwanted changes, use git cherry-pick [commit-hash] to apply only the specific fix commit.git checkout support/v1.x && git cherry-pick [commit-hash]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.| 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 |
Rebase down, Merge up. Pull changes down into your private branch with rebase. Push changes up into a shared branch with merge.
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.
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.
This is a more structured approach for team environments:
chore/framework-upgrade.develop into the upgrade branch.
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.
main/develop branch early, reducing the “delta” between the two versions.If the framework upgrade is a massive rewrite (e.g., moving from an old monolith to a new architecture), use a proxy.
| 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. |
develop code and one that runs the upgrade branch code.package.json or NuGet versions to prevent “version drift” where a sub-dependency breaks the build.develop?It depends on when the change is needed and whether a release branch has been cut 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.
The fix goes on the release branch (if the feature is part of that release), then gets synced back to develop:
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.
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:
release/v2.0release/v2.0 (via PR, with review)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.release/v2.0 into develop so bug fixes don’t get lost.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.
If main is at v2.0 and a critical bug is found, the standard hotfix flow applies:
main: git checkout -b hotfix/v2.0.1 mainmain with --no-ff and tag the result as v2.0.1.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.
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:
git checkout -b support/v2.x v2.0.0git checkout -b patch/v2.0.1 support/v2.xsupport/v2.x, and tag as v2.0.1.main / develop if the bug also exists in the current version.The support branch lives indefinitely — as long as you have clients on that major version.
| 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 |
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.”main (or a support branch), tag the commit. The tag is the single source of truth for “what is deployed.”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:
mainAfter the hotfix branch merges into main, CI/CD deploys from the tagged commit on main. The sequence is:
hotfix/v2.0.1 merges into main.v2.0.1 is applied to the merge commit on main.main.The tag on main is what CI/CD uses to trigger deployment. The hotfix branch is deleted after the merge.
support/vX.xIf 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.1 merges into support/v2.x.v2.0.1 is applied to the merge commit on support/v2.x.support/v2.x.The tag on support/v2.x triggers deployment. main (at v3.0) is untouched.
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.
| 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.
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:
main?The merge into main is the release itself. main represents “what is in production.” Without this merge, the release never officially ships.
develop?Even if zero bug fixes were made, the release branch may still differ from develop because:
package.json version, writing release notes).develop after the release branch was cut. The merge back creates a sync point that ties the two histories together.main and develop. If the release branch was never merged back, develop’s history diverges from main’s, making those future merges messier.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.
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.
| 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 |
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.
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.
| 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 |
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.
main is also your development branch, it contains work-in-progress code. Deploying a hotfix would ship half-finished features alongside the fix.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.git diff main develop to see exactly what has been developed but not yet released. With one branch, you lose that visibility entirely.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.
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).
| 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 |
| 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 |
Major Release (v1.0.0 → v2.0.0):
Minor Release (v2.0.0 → v2.1.0):
/api/v2/reports) while keeping all existing endpoints intact.Patch Release (v2.1.0 → v2.1.1):
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.