Software Development

It's Not the Workflow, It's the Practices

Author

Spencer Dangel

Date Published

Your git workflow is probably not the problem.

Teams agonize over GitFlow vs. trunk-based vs. GitHub Flow as if picking the right one will unlock fast delivery. It won't. What determines how fast you ship is lead time for change - how long code takes to go from "dev complete" to production. It's one of four metrics from DORA, the research team that's spent over a decade studying what separates high-performing engineering orgs from the rest. Fast code reviews, trustworthy CI, frequent low-risk deploys: these are the levers that move it. The workflow just decides how much friction sits on top.

That said, workflows aren't irrelevant. The wrong one can amplify weak practices and make a struggling team feel stuck. So this post walks through the common options, what they're good for, and the hybrid I've landed on for most projects. The real argument is in the practices section at the end. That's where the actual gains live.

Now let's dive into some of the different workflows.

GitFlow


GitFlow uses a combination of long-lived and short-lived branches, with strict rules about how they relate. `main` reflects the state of production, `develop` is where feature work is stored after completion until release. Temporary branches for features, release branches, and hotfix branches all come into play as well. The flow works like this:

  • Feature work branches off `develop` and merges back into `develop`
  • When `develop` has enough changes for a release, cut a release branch from `develop` and deploy it to staging
  • New feature work continues flowing into `develop` in parallel - it won't be in this release
  • Any fixes for the release go directly into the release branch
  • When the release ships, merge the release branch into **both** `main` (to reflect production) and `develop` (so the fixes carry forward)
  • For hotfixes, branch off `main` or the last release tag, fix the issue, then merge back into **both** `main` and `develop`

The benefits of GitFlow are being able to support multiple versions of a piece of software - you can have multiple different versions all deployed and maintained separately if you need to. Or if your release schedule is every two weeks no exceptions, GitFlow could be a good tool for grouping the changes for a specific release. The major problem I have with GitFlow is there's a lot of branch juggling and ceremony. You have to remember to check out `develop` before starting feature work, release branches need to get merged into both `main` and `develop`, which might involve resolving conflicts with new feature work. Hotfix branches also have to get branched from the right place and then merged back into `main` and `develop`.

Bottom line - if you're maintaining multiple live versions of your software simultaneously, GitFlow could be a good option. If you're only ever deploying to a single production environment then it's most likely not worth the headache.

GitHub Flow


GitHub Flow is a trunk based branching strategy where all work is done in a feature branch and then merged back into main:

  • One `main` branch
  • Feature branches get merged back into `main`
  • Deployments are made from the `main` branch

This strategy works the best when code reviews are completed quickly. If you've got a branch that hangs around for more than a day or two, you're likely to run into merge conflicts which slows down the whole process. Plus, if you manually deploy changes instead of having continuous deployments set up, you're going to be doing a lot of deployments.

Bottom line - GitHub Flow works when small PRs flow into production quickly and you're comfortable with every merge being immediately user-visible.

Trunk-based development (TBD)


Trunk-based development (TBD) is associated with elite delivery performance (based on DORA research). On the surface it looks nearly identical to GitHub Flow - one `main` branch that everyone commits to - but the practices underneath are what make it work.

The main flow looks like this:

  • One `main` branch, like GitHub Flow
  • Feature branches must be short-lived (think hours instead of days)
  • Small, frequent commits - a commit is a single new function, not an entire new page

What actually makes TBD work is the practices around the branching. Fast, trustworthy CI and a real automated test suite are non-negotiable; without them you'll break the trunk constantly and undo any benefit. Feature flags do the other half of the work - they decouple deploying code from releasing features, which is what lets teams ship to production multiple times a day without exposing half-built work to users. And there's a cultural piece that's harder to copy: `main` is never broken. If a bad change accidentally lands there, you drop everything until it's fixed. 

Discipline is the hardest part for this workflow. Small, isolated commits don't come naturally. A new page on the website can't be done in one commit, it has to be broken into many. If teams can't maintain that discipline they wind up with batched commits, larger changes, and the same merge pain that TBD is supposed to address.

Bottom line - TBD looks like GitHub Flow with shorter branches, but the real difference is the practices underneath: fast CI, automated tests, feature flags, observability, and the discipline to never break the trunk. With those in place, TBD is the highest-performing workflow available. Without them, you're better off on GitHub Flow.

Hybrid Git Flow/TBD Strategy


CI/CD is the part where a lot of teams struggle. I've been there - it takes time to create the automatic deployment workflows and set up good automated testing. That makes TBD hard to do effectively. For most projects the focus is on getting new features out, not working on internal tooling - even if it would pay off later. To that end, the strategy I've implemented on most of my projects is a hybrid between GitFlow and Trunk-Based Development.

Here's how it works:

  • `main` is the trunk. All feature work branches off of it and gets merged back in as soon as possible.
  • When changes are ready to be deployed to production, a release branch is cut from `main` and the changes are deployed to a staging environment for testing. 
  • Any changes made for the release to fix issues are merged back into the release branch.
  • When the release is deployed to production, the last commit in the release branch is tagged (to track the exact changes in production) and the branch is merged back into `main`.
  • Hotfixes can go one of two ways - either group them into the next release or branch off of the previous release tag. If you branch off the previous release, the hotfix changes _must_ get merged back into `main`. The third option is committing the fix into `main` and then cherry-picking the commit to the previous release branch for immediate release. I'd personally recommend against it for most teams, if you have trouble remembering to merge hotfixes back into `main` then just do the hotfix in `main` and cut a new release.

In practice, this allows for both TBD's fast integration loop for feature work and GitFlow's stable release testing while still avoiding the majority of the complex branching from GitFlow. It also meshes well with projects that don't have automated deployments configured. It's a lower bar to have automatic deployments for a QA and/or staging environment than it is for a production environment. Once everyone's happy with the state of the project in staging then a manual deployment to production shouldn't take too long.

Bottom line - The hybrid strategy is what I'd personally recommend for most teams; you get trunk-based development for day-to-day work and a release gateway while avoiding the complex merging ceremonies from GitFlow.

Practices That Matter More

As you may have noticed, as we moved from GitFlow to GitHub Flow to TBD above, more and more emphasis was placed on practices outside of the workflow like CI/CD and fast code reviews. Most of the time, switching from GitFlow to TBD is not going to improve your time to production by itself. You can run TBD and still ship slowly if PRs sit for days or your CI pipeline takes close to an hour to run. In these cases, it's not the workflow, it's the foundation that's missing. The workflow builds upon the rest of the practices your team has.

Here are some practices that help teams ship faster:

Frequent Production Releases

If releasing is painful (manual deployments, low confidence in the changes, etc), then you're going to do fewer deployments with larger change sets. This feeds into a self reinforcing loop of larger releases and larger risk. The teams with the best delivery performance release extremely small and low-risk changes, a practice that forces improvement in everything else.

CI/CD

Your pipelines need to be fast and trustworthy. A slow pipeline means you're either waiting for it to complete or context switching again when it's done to review the results. Slow pipelines also lead to batching changes to avoid waiting, meaning larger PRs, later integration, and higher chances of conflicts when merging. Brittle pipelines cause issues too - if they're breaking often or surfacing non-issues consistently, it's more of a headache than a benefit. Fast, reliable pipelines are a prerequisite for any workflow that relies on frequent integration. Good CI/CD is the foundation of an effective workflow.

Fast Code Reviews

Slow code reviews aren't a workflow problem, they're a team problem - most developers don't like reviewing code. However, code reviews can aggravate symptoms of a bad workflow. The longer it takes to review the code the longer it takes to get changes out to production. This is why teams running TBD sometimes drop code reviews entirely and rely on automated testing to catch changes. There's no big secret, doing code reviews faster is mostly just doing it in the first place. But having smaller PRs and having explicit review SLAs can help.

Monitoring and Observability

Deploying frequently isn't going to do much if you don't have a way to detect issues with the deployments. If your only warning signal is customers calling you to report issues, you can't respond to issues effectively or fast. You have to know what error is being thrown and which part of the code is throwing the error. Sentry, Elastic APM, there are plenty of tools out there to help detect issues early and allow for fast responses to issues. Being able to monitor your changes when they get deployed helps build the confidence to deploy more frequently.

Feature Flags

If there's any silver bullet for improving git workflows, it's feature flags. Feature flags decouple deploying (putting code in production) from releasing (allowing users to see the changes). Half completed features can be merged in and deployed to production without affecting the experience for the users. The trade off is a lot of tech debt with old flags. High performing teams treat removing old flags as a priority - as soon as the feature is fully turned on the flag gets removed.


Picking a Branching Strategy

Picking a new branching strategy is pretty simple:

  1. Do you maintain multiple _live_ versions of your project simultaneously? Choose GitFlow.
  2. How long does a PR typically sit before merging?
    1. A few hours: GitHub Flow or TBD
    2. A day or two with good CI: the hybrid
    3. A few days, no CI: you're probably back at GitFlow
  3. Are you comfortable deploying to production on every merge? If yes, GitHub Flow or TBD. If not, the hybrid allows for validating the changes before a release.

Ultimately, the workflow is a team agreement. Everyone needs to agree about how they're going to collaborate on the project. It's not necessarily the workflow that's causing friction, although switching to a different workflow could reduce some headaches. My advice would be to start with the practices. The workflow will follow.