DevOps · Source, Build and Delivery · Foundation · 24 min read · FREE LESSON
Git Workflows and Trunk-Based Development: Small Changes, Fast Feedback
How version control underpins delivery, the common branching models and their trade-offs, why short-lived branches win, code review habits that scale, and a practical migration plan to trunk-based development.
A story to begin with
Version control as the single source of truth
Put everything that defines your system in version control: application code, infrastructure definitions, pipeline configuration, deployment manifests, dashboards, alert rules and documentation. Benefits:
- A complete history: who changed what, when and why.
- Review before change, for everything, not just application code.
- Rollback by reverting a commit.
- Reproducibility: recreate any environment from a known revision.
- Audit evidence that comes for free.
A useful test: if your laptop and the laptops of everyone on your team were destroyed, could you rebuild and redeploy production from the repositories alone?
Branching models compared
flowchart LR
subgraph FB[Long-lived feature branches]
M1[main] --- F1[feature A, 3 weeks]
M1 --- F2[feature B, 2 weeks]
end
subgraph GF[Gitflow]
D[develop] --- R[release branch] --- M2[main]
D --- FF[feature branches]
end
subgraph TB[Trunk-based]
T[main / trunk] --- S1[short branch, under 2 days]
T --- S2[short branch, under 2 days]
end
style TB fill:#34d399,color:#062
style FB fill:#f87171,color:#fff| Model | How it works | Strengths | Weaknesses |
|---|---|---|---|
| Long-lived feature branches | Each feature lives on its own branch for weeks | Isolation while developing | Painful merges, late integration, late feedback |
| Gitflow | develop, feature, release and hotfix branches with a formal flow | Clear structure for versioned products with scheduled releases | Heavy process, long-lived branches, slow feedback; poor fit for continuous delivery |
| GitHub flow | Short feature branches, pull request, merge to main, deploy | Simple, good for web apps | Needs good tests and discipline about size |
| Trunk-based development | Everyone integrates into one main branch at least daily, via very short-lived branches or directly | Fast feedback, small changes, easy rollback, supports continuous delivery | Requires strong automated tests and feature flags |
Research on delivery performance repeatedly links short-lived branches and frequent integration with high performance.
Why long-lived branches hurt
- Merge conflicts grow with the square of the time apart.
- Integration bugs hide until the merge, when they are hardest to diagnose.
- Review quality falls with size: a 2,000-line pull request gets a rubber stamp; a 100-line one gets real attention.
- Feedback is delayed, so assumptions stay unchecked for weeks.
- Risk is concentrated in big, rare releases.
How to merge small without breaking things
The objection to small merges is "but the feature is not finished". The answer is to separate deploying code from releasing a feature.
flowchart LR A[Commit small, safe steps] --> B[Merge to main daily] B --> C[Code is deployed,<br/>feature hidden behind a flag] C --> D[Turn on for the team,<br/>then 5% then 100%] D --> E[Remove the flag when stable] style C fill:#6366f1,color:#fff
Techniques:
- Feature flags hide incomplete work and allow gradual exposure and instant switch-off.
- Branch by abstraction: introduce a seam, build the new implementation behind it, switch over, remove the old.
- Expand and contract for database changes: add the new, migrate, remove the old in separate releases.
- Dark launches: ship code that runs in production without affecting users.
- Small vertical slices: deliver the thinnest end-to-end piece of value first.
# a simple feature flag check
if flags.is_enabled("new-checkout", user=user):
return new_checkout(cart)
return old_checkout(cart)
Remember to clean up flags; stale flags become a maze. Give each an owner and an expiry date.
Commits and pull requests that help reviewers
- One logical change per commit, with a message that explains why:
Reject empty host lists in decommission script An empty filter was treated as "all hosts" by the inventory API. Refuse to run when no hosts match, and add a unit test. - Keep pull requests small, ideally under about 200 lines of changes, and focused on one thing.
- Include context: what, why, how it was tested, risks and rollback.
- Use a template so every PR answers the same questions.
Code review that is fast and useful
flowchart TD
A[Author opens small PR<br/>with context and tests] --> B[CI runs: build, tests, lint, scan]
B --> C[Reviewer within one working day]
C --> D{Feedback type}
D -- must fix --> E[Change requested with reason]
D -- suggestion --> F[Comment prefixed 'nit' or 'optional']
E --> A
F --> G[Approve and merge]
style C fill:#6366f1,color:#fffGood review habits:
- Automate the boring parts (formatting, linting, security scans) so humans discuss design and correctness.
- Respond quickly: set a team norm (for example within four working hours) so work does not wait.
- Review for correctness, clarity, tests, security and operability (logging, metrics, failure behaviour).
- Be kind and specific: critique the code, explain the reason, offer alternatives.
- Prefer pairing or a quick call to a long comment thread.
- Authors should treat review as collaboration, not judgement.
Protecting the trunk
Trunk-based development works only if main stays healthy.
- Branch protection: required status checks, at least one review for non-trivial changes, no force pushes.
- Fast CI (ideally under ten minutes) so people do not skip it.
- A broken main is everyone's top priority: revert first, fix after.
- Pre-merge checks and, for busy repositories, a merge queue that tests the combined result.
- Good tests at several levels (see the testing lesson).
Release branches and hotfixes in a trunk world
Most teams release straight from trunk. If you must support multiple released versions (for example boxed software), cut a short-lived release branch from trunk, cherry-pick fixes from trunk to the branch (never the other way round), and keep the branch's life short.
Moving your team to trunk-based development
flowchart LR A[1. Measure:<br/>branch age, PR size,<br/>merge conflicts] --> B[2. Fix CI speed<br/>and reliability] B --> C[3. Introduce feature flags] C --> D[4. Set a working agreement:<br/>branches merge within 2 days] D --> E[5. Shrink PRs with<br/>vertical slices and templates] E --> F[6. Review metrics monthly] style C fill:#6366f1,color:#fff
- Measure the starting point: average branch lifetime, pull request size, time to first review, number of merge conflicts, and the age of the oldest open branch.
- Make CI fast and trustworthy first; nobody will merge daily to a slow or flaky pipeline.
- Introduce feature flags with a simple library or service and a clean-up rule.
- Agree a working agreement: "branches live at most two days; pull requests under 300 lines; reviews within four working hours".
- Coach on slicing work: run a workshop where the team breaks a current feature into the thinnest vertical slices.
- Protect main with required checks and a revert-first rule for breakages.
- Review monthly: branch age, PR size, lead time and change failure rate. Expect lead time to fall.
- Handle releases separately: decide whether you release from trunk, with flags and canaries, or need a short-lived release branch.
Pitfalls
- Going trunk-based without automated tests, which leads to a broken main.
- Flags that are never removed.
- Declaring a policy without fixing the CI speed that makes it practical.
- Large "refactoring" branches that nobody can review; do these incrementally instead.
Interview questions
Q1. What is trunk-based development? A branching approach where developers integrate into a single main branch at least daily, using very short-lived branches or direct commits, with feature flags to hide incomplete work. It supports continuous integration and delivery.
Q2. Why are long-lived branches a problem? Conflicts and integration bugs accumulate and surface late, reviews become large and shallow, feedback is delayed and releases become big, risky events.
Q3. How do you merge unfinished work safely? Hide it behind a feature flag or an abstraction so it is deployed but not active, merge small increments daily and enable it gradually when ready.
Q4. Gitflow vs trunk-based? Gitflow uses several long-lived branches suited to scheduled, versioned releases; trunk-based favours continuous integration and delivery with a single main branch. Most web services benefit more from trunk-based.
Q5. What makes a good pull request? Small and focused, a clear description of what and why, tests, passing automated checks, and notes on risk and rollback.
Q6. How do you keep main from breaking? Branch protection with required checks, fast and reliable CI, optional merge queues, and a team rule to revert first and fix afterwards.
Next: unlock Continuous Integration Pipelines to build the automation that makes frequent merging safe.