Skip to main content
Back to module
Software Engineering Practices · Working in a Team

Branching and releases

Reading26mLesson 1 of 3 in this module

Use trunk-based development with small, short-lived branches and version releases with semver.

Why it matters

As soon as more than one person works on a codebase, you need an agreement about how changes flow from a laptop to production. Without one, people sit on long-lived branches, merges turn into day-long conflicts, and nobody is sure what is in a release. A branching strategy and a versioning scheme are that agreement. This lesson explains the most common modern approach, trunk-based development, and semantic versioning (semver), the numbering scheme used by npm and most libraries.

Long-lived branches and why they hurt

A popular older model, often called Git Flow, uses a develop branch, long-running feature/* branches, release/* branches and hotfix/* branches. It works, but it has a cost: the longer a branch lives apart from the main line, the more the two drift. When a three-week feature branch finally merges, it conflicts with everything else that landed in those three weeks, and the combined code has never been tested together. Integration problems are discovered late, when they are most expensive.

Trunk-based development

In trunk-based development, everyone integrates into one main branch (the "trunk", usually main) at least once a day.

  • Short-lived branches. A branch lives hours or a day or two, then merges through a pull request. Some teams commit straight to main with pair programming instead.
  • Small changes. Each pull request is small enough to review in minutes.
  • Continuous integration (CI). Every push runs the build and tests automatically. main must always be releasable; if CI breaks, fixing it is the team's top priority.
  • Feature flags for unfinished work. Code for a half-built feature can merge to main hidden behind a flag that is off in production. You release when it is ready by flipping the flag, not by merging a giant branch.
if (flags.isEnabled("new-checkout", user)) {
  return renderNewCheckout(cart);
}
return renderCheckout(cart);

A typical day looks like this:

git switch main && git pull
git switch -c fix-tax-rounding
# small change + test
git commit -am "fix(cart): round tax to cents"
git push -u origin fix-tax-rounding   # open a PR, CI runs, a teammate reviews
# merged the same day, then the branch is deleted

Releases in trunk-based development are cut from trunk: either every merge deploys automatically (continuous deployment), or the team tags a commit on main and deploys that tag. If a release needs an urgent fix, the fix goes to main first and is then cherry-picked to a short release branch if one exists.

Feature flags have a cost too: every flag is a branch in the code. Remove a flag once the feature is fully launched.

Semantic versioning

Software that other code depends on (libraries, APIs, apps with public release notes) needs version numbers that say what changed. Semantic versioning uses MAJOR.MINOR.PATCH, for example 2.4.1:

ChangeBumpExample
Breaking change: existing callers may need to change their codeMAJOR2.4.1 -> 3.0.0
New feature that is backward compatibleMINOR2.4.1 -> 2.5.0
Backward-compatible bug fixPATCH2.4.1 -> 2.4.2

Rules worth remembering:

  • When you bump a number, reset the ones to its right to zero: 2.4.1 plus a new feature is 2.5.0, not 2.5.1.
  • 0.y.z means "initial development": anything may change at any time, and many tools treat a minor bump as possibly breaking.
  • Pre-releases use a hyphen: 3.0.0-beta.1 sorts before 3.0.0.
  • Removing a function, renaming a field in an API response or making an optional parameter required are all breaking changes, even if they feel small.

npm uses semver ranges in package.json. "^2.4.1" accepts any 2.x.y at or above 2.4.1 (no new major). "~2.4.1" accepts only 2.4.x patches. The lock file (package-lock.json) records the exact versions installed, so every machine and CI run gets the same ones.

Teams often generate versions and changelogs from commit messages that follow Conventional Commits (feat: means minor, fix: means patch, BREAKING CHANGE: or feat!: means major), which ties the two halves of this lesson together.

Common mistakes

  • Branches that live for weeks "until the feature is done".
  • Merging to main with CI red, or ignoring a broken build.
  • Releasing a breaking change as a minor or patch version.
  • Forgetting to remove feature flags after launch.
  • Deleting the lock file to "fix" an install problem, which silently upgrades dependencies.

Recap

  • Trunk-based development: small, short-lived branches merged to main daily, CI on every push, flags for unfinished work.
  • Releases are cut from trunk; fixes land on main first.
  • Semver: MAJOR for breaking changes, MINOR for compatible features, PATCH for compatible fixes.
Also in this lesson: