Sam Crossland · 24th July 2025

Beginner’s guide to Git branching strategies

Branches, commits, pull requests, merging, conflicts — these are familiar terms we hear across the wider software engineering world, and within high-performing Salesforce teams. But tying them all together into a strategy can be tricky. With long-lived sandboxes and varied metadata types, Salesforce’s unique development model can make it even harder to adopt a branching strategy that works for everyone, adding to the initial learning curve.

In this blog post, DevOps Architect Sam Crossland takes a closer look at what branching is and what it means in a Salesforce DevOps context. He’ll cover the key definitions and concepts you need to know, explore the different types of branching strategies, and how they might evolve as you and your team mature through the DevOps process. Finally, he’ll run through a real-world scenario to show how Git branching plays out in practice — wrapping up with some common branching mistakes and how to avoid them.

Git branching basics

Version control has been a fundamental part of software development for a long time. It’s what makes branching strategies possible in the first place. Salesforce has traditionally leaned towards a “clicks not code” approach. But as the platform has evolved with more complex features, coding languages, and metadata types, version control has become increasingly important for Salesforce teams too.

Bringing version control into your development process opens up many powerful capabilities. You can track changes to files over time, see who made those changes when, and understand how those changes moved through the environments in your release pipeline. It also helps your team collaborate more effectively and unlocks the ability to automate parts of your workflow.

Branching strategies work as a layer above your version control system (VCS). They define how your team works with branches for different types of changes, like new features, urgent hotfixes, or larger projects. A good branching strategy helps you link your branches to Salesforce sandboxes, manage how work gets merged and promoted, and set clear expectations for quality and collaboration at each stage of the development lifecycle.

To learn more about version control, check out DevOps Launchpad’s free version control fundamentals course.

There are some key higher level concepts and terminology to understand around branching and how it works alongside your VCS:

  • Repository (repo): Common space or storage area for developers to work on the same codebase together.

  • Commit: Taking some work and “saving” it locally or in the central repository, which could affect 1 or more files.

  • Branch: Forking off the main stream of work (usually main or master) into a new separate area to be worked on standalone (usually a feature).

  • Pull request (PR): Raising a request in your VCS to merge one branch into another, enabling peers to review the changes and approve/decline.

  • Merging: Taking the subcomponents and attempting to make them fit together — merging one branch into another.

  • Conflict: Where an attempted merge contains differing entries for the same files/lines, and Git can’t automatically resolve the differences.

As part of your branching strategy, you’ll often see certain types of branches used consistently. These usually follow a prefix format like “branchType/branch-name”, and help clarify the purpose of the work in progress. Here’s a quick breakdown of the most common types:

  • main/: The main branch is your source of truth — it represents the production-ready version of your Salesforce codebase. All deployments to production should come from this branch.

  • develop/: This is a long-lived integration branch, commonly used in structured workflows like GitFlow. It’s where all your completed features are merged ahead of a major release. It typically follows a regular release cadence, sending changes to sandboxes and eventually to production.

  • feature/: Feature branches are used to build new capabilities or enhancements. They’re short-lived branches that, once tested and complete, are merged back into main or develop. Teams often include the ticket number to link the branch to a story or task e.g., feature/TICK-123-new-opp-screen or feature/TICK-321-flow-logging-improvements.

  • bugfix/ or hotfix/: These branches are for fixing bugs — whether they’re critical or minor. They often branch off main, and merge back in once the fix has been tested. Some teams use bugfix/ for general issues and hotfix/ for urgent, production-impacting problems e.g., bugfix/report-filter-issue or hotfix/flow-validation-error.

  • release/: A release branch is used to prepare a group of changes for deployment as a single release. It’s created from develop (in GitFlow) or main (in simpler setups). Teams use it to test, refine, and fix last-minute issues before going live e.g., release/q3-release or release/v1.2-sprint-end.

The branching evolution

Now that you’ve seen the common branch types and how they’re used in version control, let’s take a look at how the strategy part comes into play. This is where you decide how those branches work together — based on your team size, how often you want to release, your quality expectations, and how much complexity you can handle.

In this next section, we’ll walk through some of the most common strategies out there, in order of DevOps maturity. We’ll look at how they work, why teams adopt them, and the trade-offs to consider. We’ll also cover the signs that it might be time to evolve your approach — helping you shape a branching strategy that supports your team’s growth and performance over time.

Git as a backup

Git as a backup

This is one of the simplest strategies to get into using version control and branches, but the downside of that is that you don’t really get a lot of the core benefits:

  • How: We have a long-standing branch per environment (QA, UAT, Staging, etc.), that is regularly backed up via an automated process (like a Gearset CI job), with specific metadata. The direction is from org > git only, and deploying org > org will still be manual.

  • Why: This can give some confidence in the state of your metadata being stored off platform and present in a branch, even if you’re not getting the main benefits of version control. This is usually appropriate if you’re a small team stepping into source control for the first time and don’t feel confident in other strategies.

  • Considerations: Since we’re not using version control to store and review code, trigger automated tests or deployments, and the items we back up may not actually be able to deploy back into the sandboxes, we’re losing out on a lot of the core benefits of a CI/CD process here.

  • Evolution: If you want to truly use branches as a source of truth, unlock parallel development, help automate deployments, and streamline your CI/CD strategy, you’ll want to move to a more mature strategy.

Feature branching

Feature branch model

This is the real starting point for collaborative development and using Git’s core power to manage changes effectively, in a simple, low-overhead form:

  • How: Each developer creates an isolated feature branch (e.g. feature/add-custom-discount) from the main branch for their specific task. They develop, commit changes to their feature branch, and push. Once their work is complete and tested, they open a pull request (PR) for code review. After approval and any automated checks, the feature branch is merged into main, which can be aligned to a number of testing sandboxes, along with your production environment, and deployed via automated means.

  • Why: This strategy enables multiple developers to work on the same files simultaneously, without directly overwriting each other until integration. Then, it would become a merge conflict to be resolved between team members. This strategy isolates work, provides a clear history for each feature branch, and is the foundation for code reviews via PRs — as well as automating your deployments.

  • Considerations: All features end up merging directly into the main branch. If numerous features are in progress, main can become a bottleneck for releases. It could also become unstable if testing isn’t rigorous enough before merging, or if features sit around in your testing environments for too long. Merge conflicts are resolved when integrating into main, which can be challenging if features diverge significantly, or the branches have been around a long time without being synced.

  • Evolution: If your main branch frequently becomes unstable due to multiple merges happening at the same time, or you need more structured environments and quality gates before features hit your core production codebase, you'll want to investigate moving to a strategy with expanded branches to give you flexibility.

To learn more about the feature branch model, check out DevOps Launchpad’s free feature branch model course.

Protected master branch

Protected master branch

This is a natural progression of the feature branch strategy, adding one more long-lived branch in play for “protection”, but does come with its own nuances:

  • How: An extra branch (usually integrate) is added as a stepping stone to get your features towards main or production. This means after a developer has created their feature branch from main and is ready to move the work forward, they PR it to integrate first. Once the testing has completed for a build up of features in your testing sandboxes, the entire integrate branch is then PR’d into main, taking all of those merged components forward to production.

  • Why: This approach gives you a chance to catch issues earlier by adding quality checks like code reviews and automated testing before anything reaches your main branch. It also keeps in-progress work separate from your production code, so you don’t risk breaking anything live. By adding two stages of review (including testing in UAT) you can be more confident that only fully tested and approved work moves into production as one complete, stable unit.

  • Considerations: This strategy can make it more difficult to modify what’s “good” or “bad” in your central integrate branch, usually requiring you to take features out, or only progress known good features forward. This can be tricky for non-git-savvy team members. It also puts a bit of a bottleneck on new work that can merge in and deploy into integrate or UAT while you resolve the release issues.

  • Evolution: If you’re seeing features get stuck waiting in a queue, struggling to roll changes back to unblock the path from integration to main, or wishing you had more flexibility and quality checks as stories progress — this model can start to feel limiting.

Expanded branching

Expanded branching

When you need more control over the state of each phase of your workflow, and the ability to keep features separate throughout the route to main, this model works in your favor:

  • How: Each developer works on their own feature branch, which is a copy of the main branch created just for their specific task. They make changes, test them locally, and then push those changes to the shared repository. When their work is ready, they open a PR to merge their branch into the first environment branch (usually integration). This step includes a code review and automated checks to catch issues early. Once approved, the feature is merged and automatically deployed to a testing sandbox that’s linked to that environment. From there, the process is repeated: the code moves through other environment branches (like UAT), with more reviews and testing at each step, until it reaches the main production branch. At that final stage, you might group several features together into a release branch to make it easier to deploy them all at once.

  • Why: The main reason you’d want to use this model is having total freedom and control over how quickly an individual feature could propagate through your environments and their respective quality gates. It also enforces a very similar sandbox > sandbox promotion process (as you would with change sets) with visible states of each environment and flexibility. Aligning specific testing/actions to specific branches/environments becomes much easier in this set up.

  • Considerations: Since there are 1:1 mappings for branches, there are arguably more branches to manage and keep in sync. For example, if a hotfix goes straight to main or production, you’ll need to update those other branches. Building on in-flight or longer-term work can be difficult in this baseline setup since every feature is deliberately isolated.

  • Evolution: Moving on from this branching strategy depends on how your team prefers to work and what kind of benefits you’re looking for. Some teams want more flexibility — so they keep features separate for longer, and only bundle them into a release later in the process. Others prefer to group work together earlier and release everything as one unit, often on a fixed schedule. This second approach is what we’ll look at next with GitFlow.

GitFlow

GitFlow

If you need faster early feedback and tightly coupled bundles of features to progress and test in your Salesforce sandboxes, GitFlow could be a route to take:

  • How: With GitFlow, your team works with two main long-running branches instead of just one for each Salesforce environment. The main branch still represents your live production environment. But you also have a develop branch, which acts as the starting point for new feature work. Developers create feature branches from develop, work on their changes there, and then merge them back once they’re ready. This code is typically deployed to testing sandboxes and follows a more structured development cycle. When a group of features is ready to go live, you create a release branch from develop. That branch can be deployed to other testing environments like staging, and once it passes all checks, it gets merged into main and deployed to production. If an urgent fix is needed in production, a hotfix branch is created directly from main, so it can be deployed quickly without waiting for the next full release from develop. After a change goes live, it’s common to “tag” the commit with a version number — this helps track exactly what was released and when.

  • Why: In this model, teams start by integrating work early — building on top of each other’s changes from the beginning and planning to release everything together as one unit. This approach makes it easier for teams to collaborate closely, especially when working on related features. You get fast feedback because everyone’s changes come together early in the process, without needing to go all the way to production. This is especially helpful for larger teams working on connected tasks. When a group of features is ready, you create a release branch. That branch then moves toward production, while new work can continue on the develop branch without being held up.

  • Considerations: Because all the work is grouped together early in the develop branch, it can be harder to move just one feature forward or separate certain changes when you’re ready to create a release. That means you’re committing to having develop in a stable, combined state before anything can move on. There are also several coordination points to keep in mind. For example, if a hotfix is merged directly into main, you’ll need to make sure develop gets updated too. Or if the release branch runs into a testing issue, it may need a fix — but develop might have already moved ahead with the next round of changes. In larger teams with lots of sandboxes and multiple branches, keeping everything in sync can add extra complexity. Different branches might start to drift apart, which can lead to more work managing merges and avoiding conflicts.

  • Evolution: As referenced above, the weigh up between a strategy like GitFlow and another like the expanded branching mainly comes down to how early you want to tightly couple your work together, and whether you want to adhere to a quite fixed release schedule vs. “as-and-when” feature promotion. If you want to be more agile and flexible in your feature creation and deployment through to production, then the expanded branch strategy may be more suitable.

Honorable mentions

There are a few other branching strategies out there that deserve a quick mention too:

  • Trunk: Trunk-based development is quite similar to the feature branch model referenced above, but goes one step further in recommending we have minimal branches “live” in our repository at any one time. This means no long-lived feature or release branches, and usually dictates direct commits into the main branch rather than PRs. A key part of this strategy is committing to the main branch frequently and keeping it in a deployable state at all times. It’s often used in fast-paced teams that prioritize speed and continuous delivery. However, this high velocity can come at the cost of fewer opportunities for quality checks before changes go live.

  • Scaled trunk: This is a variant of trunk-based development that includes feature and release branches to scale up the flexibility, but loses some of the core tenets of the strategy.

  • GitLab Flow: GitLab Flow is a branching strategy created by GitLab to address some of the common challenges found in GitFlow and GitHub Flow. It’s flexible and can be used in a few different ways, depending on how your team works. One key idea behind GitLab Flow is using long-lived branches that represent different environments (like staging or production) or even specific release versions that exist outside your team (such as what’s been delivered to a customer). It follows a state-based approach, where each branch is meant to reflect the exact state of a particular environment or release. This makes it easier to track what’s deployed where and manage changes across multiple versions.

A real branching journey

We’ve talked a lot about the theory of different branching strategies, but let’s see one in action. We’re going to pick the expanded branching strategy for this, as it easily showcases the iterative progression through your sandboxes on the way to production. We’ll also highlight some key areas where other strategies would be notably different.

Step 1

As a developer on my team, I’ve been allocated a new Jira ticket, SP-55, to look at expanding some functionality we have in an existing Apex class. The base class already exists in the repository so I need to make sure I pull the latest changes first and start from the right foundation to prevent potential merge conflicts later.

Jira ticket

Step 2

I can open up my Integrated Development Environment (IDE), which in this case is VS Code. Since I know we use the expanded branching model, I need to use main as the base branch, and ensure everything is in sync first before cutting my new branch.

Note: If I was using GitFlow, I’d likely be creating my new branch from develop instead.

I use a number of Git commands to manage this, including:

  • git status— check how far ahead or behind my current local version is from the remote repository.

  • git pull— a combination of fetch and merge, this makes sure I pull down the latest set of changes locally.

  • git checkout -b <branch-name>— usually git checkout is used to switch branches, but the -b flag creates and checks out a new branch in one step.

Initial repo check and new branch

Step 3

Now I’m in sync with what’s been happening in the remote repository, and in a new clean branch area to start my work. I can open up the existing class file and start working on the new method. Since I know quashing any security vulnerabilities or performance issues early on is much better than finding them later, I can use something like Code Reviews or Salesforce Code Analyzer to scan my new method inside the IDE and aim to fix any important issues straight away.

Change made and SCA run

It also gives me the results of any technical debt in the rest of the file, which I’m unlikely to be able to resolve as part of my scoped task. However, I can flag this and backlog to prioritize at a later time.

Step 4

I can start by using Agentforce for Developers to suggest quick fixes and use those as a foundation to improve my code. Once I’ve accepted the recommended changes and rechecked the code analyzer results, I’m ready to commit the updates to my local feature branch.

I use git add -A and git commit -m "<Message>" for this, which means “add everything that’s changed in my local files, and save those changes into a new commit with the message "<Message>".

Note: The changes are still local for now.

Agentforce quick fix
Class SCA fixes made
Initial Git commit

Step 5

Since I’ve undertaken this development in the IDE, I can use the Salesforce CLI to deploy those specific changes into my sandbox so they’re visible, and then loop back to Agentforce to think about the right test cases I need to prove this new method will work as expected.

Salesforce project component push to org
Salesforce class visible in org
Agentforce generate tests

Step 6

Once the tests are generated, tweaked, code scanned again, pushed into my sandbox, and run remotely so I know they provide the right coverage and results, we need to add a new commit to the feature branch.

I add a new Git push command onto the same line here (using semi-colons as the delimiter) which takes my local branch or commits and pushes them into the remote repository.

Sometimes Git will complain about knowing where the upstream is to push to, and, depending on your configuration, you may need to add upstream (tracking) reference (--set-upstream <BranchName>) to get around this.

Tests made and passed
Commit tests in one line

Step 7

We’ve now got my feature branch into GitHub, and can raise a PR against the first environment in my sandbox workflow, which is integration in my case. Here, I make sure to provide the right level of detail in the PR description (in accordance with any PR template you may have), which can be super useful in setting up some consistency across your repo.

GitHub PR raised

Step 8

When this pull request is raised, we can see several automated checks being triggered — like policy scans or validations against the target org. In this example, validations are done via Gearset, but it could just as easily be handled using GitHub Actions with Salesforce CLI commands. Reviewers are also added automatically at this stage.

This first PR is a key opportunity to catch issues early — whether that’s through automated checks or feedback from team members who can spot logic errors or suggest improvements.

Pull request part one

Step 9

I can then merge this PR (using merge commit here so we keep the best history of commits) and depending on my CI/CD setup, this would then trigger a deployment to the sandbox in question.

Pull request part two: merge
Gearset CI job run

Step 10

As I’m using the expanded branch model, the process is now repeated for each subsequent environment, all the way up to main and production. So I repeat steps 7-9 above for both UAT and main, handling any merge conflicts or validation issues that may crop up, and iteratively merging and deploying until it reaches production.

PR to UAT
PR to UAT: ready to merge

Note: If we were using GitFlow, the team would’ve likely combined numerous features into develop first, and then cut a release branch bundling those changes together to progress to later environments.

Step 11

We can then see the updated class and test class present in production, showing we’ve progressed our initial feature branch all the way from conception through to reality!

Main CI job validation
Main PR merge
Live in production

Branching nuances and things to watch out for

Now that we’ve explored the different branching strategies and walked through what a typical journey across key phases might look like, it’s time to dig into some of the more nuanced aspects of version control. We’ll also cover a few common pitfalls teams often run into — and how you can avoid or work around them. Let’s dive in:

  • The “eternal” feature branch: Long-lived feature branches can lead to merge hell as you progress it into your long-standing environment or central branches, so keeping features as short-lived as possible is the best way to counter this. Certain situations, like running a longer-term project, obviously require a longer time period where that branch needs to exist and before it can merge through, and in those situations defining a long-term project branch could be the answer.

  • Branching from outdated code: When working locally in your IDE, and with larger teams, you don’t always know how far along your main or base branch is in the central repository. Regular “Git pull“ commands should be leveraged to ensure you’re kept up to date before creating new branches. This also ties in with making sure you remember to actually push your changes! They’re only local and viewable by yourself until you use “Git push” to get them into your remote repository, where you can then raise a PR to get some team feedback and kick off automated testing.

  • Overlooking the power of PRs: One common mistake teams make is using pull requests purely for merging code, without fully taking advantage of what they can enable. When PRs aren’t used effectively, key opportunities are missed — like getting the right people involved in code reviews, sharing knowledge across the team, or enforcing consistent development standards. Without a proper PR process, you also lose a valuable audit trail of how a feature has progressed across branches and environments, making it harder to trace changes or debug issues later. Critical branches like main or develop can even be put at risk if team members bypass PRs and commit directly. Teams that don’t use PRs as a trigger for automated workflows — such as testing, security scans, or deployment validations — miss a key chance to build quality checks into the process early. Without these guardrails, bugs and inconsistencies are more likely to slip through.

  • Letting old branches pile up: Keeping every feature branch forever might not seem like a big deal at first, but over time it can clutter your repository and slow things down. A bloated repository makes it harder to find what you need, increases clone times, and can even lead to more merge conflicts. Without a cleanup routine, stale branches hang around far longer than they’re useful. The fix? Define an approach early — whether it’s manual or automated — that helps you identify and remove outdated branches before they become a problem.

  • Ignoring the impact of merge methods: Choosing how you merge might seem minor, but it affects auditability, collaboration, and your project history. Many teams stick with the default without understanding the trade-offs. If you don’t align on a merge strategy upfront, you can end up with a messy Git history that’s hard to navigate (or debug) later on. You’ll usually come across three — merge commit, squash, and rebase:

    • Merge commits are best for traceability, showing exactly what was merged and when.
    • Squash merges create a cleaner history but lose detail about individual commits.
    • Rebasing keeps things linear, but can be risky — especially with shared branches — because it rewrites commit history and can lead to tricky conflict handling.

Wrap up

As you can see, there’s plenty of well-trodden paths for using version control and getting a branching strategy in place for Salesforce development, and a lot of aspects to consider in making the right decision for your team, but don’t let that put you off! You can see this as a journey and evolve the way your team works over time as you scale into different contributors, velocity of development, or requirements around coupling work together. This is a perfect example of one of the key tenets of DevOps and Agile methodologies; continuous improvement.

You also don’t necessarily need a paid DevOps tool to achieve these; plenty of members in the community use free tools like GitHub Actions or ADO Pipelines to achieve an automated way of working with various branches and Salesforce sandbox deployments. There are, of course, some downsides to doing this “DIY” approach, mainly around ongoing maintenance and feature disparity vs. paid platforms, but it’s certainly possible to implement these branching strategies yourself.

To learn more about adopting Salesforce DevOps principles by building or buying a solution, check out Gearset’s free build vs. buy whitepaper.

What next?

Solidify your new Git branching knowledge by enrolling in the free, self-paced Git branching strategies course on DevOps Launchpad. You’ll have the chance to prove your Git branching skills, all while earning your next DevOps certificate.