Trunk-Based Development: Shipping Faster with Short-Lived Branches
AI generated
git
HEAD
Git · Trunk-Based Development · CI/CD · Feature Flags
Trunk-Based Development
Shipping Faster with Short-Lived Branches

Trunk-based development replaces long feature branches with short-lived ones that flow back into the main branch several times a day. Feature flags make it possible to ship unfinished work safely without endangering the application, while a fast, reliable CI pipeline checks every commit for regressions immediately and keeps the main branch permanently releasable.

14 min. read Short-Lived Branches · Feature Flags · CI/CD Git · Git Flow · GitHub Flow

1. What trunk-based development actually solves

Most Git workflows built around long-lived feature branches solve a problem while simultaneously making it worse: isolation. A branch that exists in parallel to the main branch for a week or longer inevitably drifts apart from it. Other developers keep committing, dependencies change, and the eventual merge becomes a risk instead of a formality. Merge hell is not caused by bad developers, it is caused by the time span between branch creation and integration. The longer that span, the larger the space for conflicts.

Trunk-based development (TBD) inverts the principle: instead of integrating as late and as rarely as possible, integration is forced to happen as early and as often as possible. Every developer commits directly, or through very short branches, into the main branch, the so-called trunk, multiple times a day. The result is not an accident, it is a deliberate shift of risk: small, frequent integrations instead of rare, large ones. For PHP and Magento teams with several developers working on the same store codebase, this is a direct lever against the classic merge conflicts in layout XML, di.xml, and Composer lock files.

2. The philosophy: very short-lived branches

The core of trunk-based development is a simple rule: a branch lives hours, at most one or two days, never weeks. This rule is not arbitrary, it follows directly from the mathematics of integration: a branch merged two hours after creation can barely have diverged from the trunk in any meaningful way. A branch that lives for two weeks has inevitably missed hundreds of commits from other people. Developers therefore break their work into smaller, independently shippable units instead of building an entire feature in one single, giant branch.

In practice this means git pull --rebase before every new block of work, a new branch per subtask, and a merge back into the trunk as soon as the change is tested and reviewed, not only once the whole feature is finished. This discipline feels unfamiliar at first because it forces large tasks to be broken into small, self-contained steps. That decomposition is exactly the real value: it makes every single step reviewable and testable, instead of checking a thousand-line diff at the end that nobody can fully grasp anymore.


#!/usr/bin/env bash
# Typical short-lived branch workflow for trunk-based development
set -euo pipefail

# Always start from an up-to-date trunk
git checkout main
git pull --rebase origin main

# Create a short-lived branch for one small, self-contained change
git checkout -b feature/checkout-shipping-label-fix

# ... make the change, commit in small logical steps ...
git add src/app/code/Mironsoft/Checkout/Model/ShippingLabel.php
git commit -m "Fix shipping label rendering for split shipments"

# Rebase onto the latest trunk before opening the pull request
git fetch origin
git rebase origin/main

# Push and open a small, fast-to-review pull request
git push -u origin feature/checkout-shipping-label-fix

# After CI is green and review is approved, merge quickly
git checkout main
git pull --rebase origin main
git merge --no-ff feature/checkout-shipping-label-fix
git push origin main

# Delete the branch immediately, it has done its job
git branch -d feature/checkout-shipping-label-fix
git push origin --delete feature/checkout-shipping-label-fix

3. Feature flags as the enabler

Short-lived branches alone only solve half the problem. The real challenge remains: what happens to a feature that, after two days of work, is still not complete but still needs to go into the trunk? This is exactly where the feature flag comes in. A feature flag is a condition in the code that decides whether a new code path is active or not, independent of deployment. Unfinished code can therefore be merged, deployed, and even shipped to production without a single user ever seeing it.

This fundamentally changes the relationship between deployment and release: deployment means code sits on the server. Release means a feature is visible or active for users. In trunk-based development, these two events are deliberately decoupled. A Magento team can integrate a new checkout feature into the trunk over several days in small steps while it stays inactive behind a flag, and then switch it on through a configuration change once it is fully finished and tested. No deployment, no rollback risk, just a toggle.


<?php

declare(strict_types=1);

namespace Mironsoft\Checkout\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

/**
 * Simple, config-driven feature flag check for the new express checkout flow.
 */
class FeatureFlags
{
    private const XML_PATH_EXPRESS_CHECKOUT_ENABLED = 'mironsoft_checkout/features/express_checkout_enabled';

    /**
     * @param ScopeConfigInterface $scopeConfig Magento configuration reader
     */
    public function __construct(
        private readonly ScopeConfigInterface $scopeConfig
    ) {
    }

    /**
     * Checks whether the express checkout feature is enabled for the current store.
     *
     * @param int|null $storeId Store view id, null for the current store
     * @return bool True if the feature flag is active
     */
    public function isExpressCheckoutEnabled(?int $storeId = null): bool
    {
        return $this->scopeConfig->isSetFlag(
            self::XML_PATH_EXPRESS_CHECKOUT_ENABLED,
            ScopeInterface::SCOPE_STORE,
            $storeId
        );
    }
}

4. Feature flag patterns in practice

The simplest pattern is the binary config flag: a single boolean in system.xml, read via ScopeConfigInterface, as shown in the example above. That is enough for most internal features and allows enabling or disabling per store view without a code change. For riskier changes, a simple on-off switch is not enough, which is where staged rollouts come in: a feature is first shown to a small percentage of customers or a specific customer segment before it is enabled for everyone.

A workable pattern for percentage-based rollouts uses a deterministic hash of the customer or session ID, so the same user consistently sees the same variant on every visit, with no random flip-flopping between old and new behavior. It is also essential to consistently remove flags from the code once a rollout is fully complete: a feature flag is a temporary tool, not a permanent configuration system. Codebases with dozens of forgotten flags become a source of complexity and testing overhead in their own right.


<?php

declare(strict_types=1);

namespace Mironsoft\Checkout\Service;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

/**
 * Feature flag service supporting boolean flags and percentage-based rollouts.
 */
class FeatureFlagService
{
    /**
     * @param ScopeConfigInterface $scopeConfig Magento configuration reader
     */
    public function __construct(
        private readonly ScopeConfigInterface $scopeConfig
    ) {
    }

    /**
     * Determines whether a feature is active for the given customer,
     * based on a deterministic percentage rollout.
     *
     * @param string $flagCode Config path segment identifying the flag
     * @param string $customerIdentifier Stable identifier, e.g. customer id or session id
     * @return bool True if the feature is active for this customer
     */
    public function isActiveForCustomer(string $flagCode, string $customerIdentifier): bool
    {
        $rolloutPercentage = (int) $this->scopeConfig->getValue(
            sprintf('mironsoft_features/%s/rollout_percentage', $flagCode),
            ScopeInterface::SCOPE_STORE
        );

        if ($rolloutPercentage <= 0) {
            return false;
        }

        if ($rolloutPercentage >= 100) {
            return true;
        }

        // Deterministic bucket: same customer always lands in the same bucket
        $bucket = crc32($flagCode . $customerIdentifier) % 100;

        return $bucket < $rolloutPercentage;
    }
}

5. Why trunk-based development needs strong CI

Trunk-based development stands or falls on one precondition that is rarely emphasized clearly enough: the trunk must be deployable practically at all times. This is not an optional best practice, it is the load-bearing pillar of the entire model. Without it, the practice of frequent integration collapses immediately. If multiple developers merge into the same branch several times a day, every single change must be automatically checked for regressions before it reaches the trunk. Manual review alone is not sufficient for that.

Concretely, that means a continuous integration pipeline that runs on every push, finishes within a few minutes, and reliably distinguishes real failures from flaky tests. A CI suite that takes 45 minutes does not just slow development down, it tempts teams to batch up reviews and merges, sliding right back toward long-lived branches. PHPUnit tests, static analysis with PHPStan, and an automated build step therefore belong in every pipeline meant to seriously support trunk-based development. In a working trunk-based setup, a red build blocks every further merge until it turns green again.


# .github/workflows/trunk-ci.yml
# Fast, mandatory checks on every push to main and every pull request
name: Trunk CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  fast-checks:
    runs-on: ubuntu-latest
    timeout-minutes: 8
    steps:
      - uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          coverage: none

      - name: Install dependencies
        run: composer install --no-progress --prefer-dist

      - name: Static analysis (PHPStan level 5)
        run: vendor/bin/phpstan analyse app/code --level=5

      - name: Coding standards
        run: vendor/bin/phpcs --standard=Magento2 app/code

      - name: Unit tests
        run: vendor/bin/phpunit --testsuite unit --stop-on-failure

      - name: Fail fast on red build
        if: failure()
        run: echo "Build is red, merge to main is blocked until fixed" && exit 1

6. Testing discipline: pre-commit hooks, reviews, small PRs

CI alone is not enough if broken code makes it to the pipeline in the first place. A pre-commit hook that runs linting, static analysis, and the fastest unit tests locally catches a large share of mistakes before they are ever pushed. This does not just reduce load on the CI infrastructure, it also shortens the feedback loop for the developer from minutes to seconds, a decisive difference when there are several merges a day.

Equally important is the size of the pull requests themselves. A PR with 50 changed lines can be thoroughly reviewed in ten minutes; a PR with 2000 lines will either sit untouched for days or get rubber-stamped, and both undermine the core idea of trunk-based development. Teams that consistently keep PRs small often combine this with pair programming or mob programming for riskier sections of code: when two developers write a change together, the downstream review effort is often minimal, because the review effectively already happened while the code was being written.


#!/usr/bin/env bash
# .git/hooks/pre-commit - fast local gate before code reaches CI
set -euo pipefail

echo "Running fast pre-commit checks..."

# Only lint and analyse staged PHP files, not the whole codebase
staged_php_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.php')

if [[ -z "$staged_php_files" ]]; then
  echo "No PHP files staged, skipping checks"
  exit 0
fi

echo "$staged_php_files" | xargs -r vendor/bin/phpcs --standard=Magento2

echo "$staged_php_files" | xargs -r php -l

vendor/bin/phpstan analyse $staged_php_files --level=5 --no-progress

echo "Pre-commit checks passed, proceeding with commit"

7. Trade-offs: when trunk-based development is not a fit

Trunk-based development is not a universal replacement for every workflow. Teams without a resilient CI infrastructure, without automated tests, or with very little shared experience working in Git risk turning an unstable trunk into the norm, which is the exact opposite of the intended goal. A team that is just learning to write clean commits and resolve merge conflicts often benefits more from a bit more isolation until basic Git skills have solidified.

Regulated industries with fixed, audited release schedules, such as payments or healthcare, also run into limits: when every production change must pass through a formal approval process, the high merge frequency of trunk-based development collides with that approval process. In such cases, a hybrid approach makes sense: trunk-based development for daily work, combined with release branches or feature flags that allow controlled, scheduled activation without falling back to long development branches. The problem rarely lies in the principle itself, but in adopting it without the infrastructure it requires.

8. Migration: moving to trunk-based development step by step

The switch rarely happens overnight, and it should not. The first step is almost always the CI pipeline: before branches get shorter, an automated, fast test run needs to exist that reliably reports green or red on every push. Without that foundation, every further step only produces more risk. In parallel, it is worth auditing existing test coverage: where are automated tests missing for critical paths like checkout or payment processing, and where can coverage be added with reasonable effort?

After that comes a gradual shortening of branch lifetimes: from two weeks to one week, from one week to two days, until branches are routinely merged within 24 hours. In parallel, a minimal feature flag infrastructure is introduced. Even a simple config-based flag mechanism like the one shown above is enough as a starting point. The key is bringing the team along: pull request size, commit frequency, and review speed are habits that settle in over weeks, they do not change from one day to the next by decree.

9. Trunk-based development compared side by side

For PHP and Magento agencies running several parallel projects with varying team sizes, it is worth comparing the common workflows directly. None of the three approaches is inherently superior, the right choice depends on CI maturity, release cadence, and team size.

Dimension Trunk-Based Development Git Flow GitHub Flow
Branch lifetime Hours to 1-2 days Weeks to months Days to 1 week
CI requirement Very high, mandatory Moderate is sufficient Highly recommended
Release cadence Multiple times a day possible Fixed release cycles Continuous, per PR
Structural complexity Low, one branch type High, many branch types Low to moderate
Handling unfinished work Feature flags in the trunk Isolation in the feature branch Usually merged only when done
Fit for small, inexperienced teams Risky without CI maturity Well suited Well suited
Fit for regulated releases Only with release flags/branches Very well suited Conditionally suited

In practice: Git Flow remains sensible for Magento projects with fixed, planned release windows and several versions maintained in parallel. GitHub Flow is a solid middle ground for teams with good, but not perfect, CI maturity. Trunk-based development delivers its full benefit only once the CI pipeline, test coverage, and feature flag infrastructure are already solid, but then it delivers the highest achievable shipping frequency together with a lower merge conflict risk.

Mironsoft

Git workflows, CI/CD pipelines, and feature flag architecture for Magento teams

Ready for faster, safer releases?

We analyze your Git workflow, build a CI pipeline that reliably keeps the trunk green, and introduce a feature flag architecture that lets your Magento team ship unfinished work safely.

CI/CD setup

Fast, reliable pipelines with PHPStan, PHPUnit, and automated deployment

Feature flag architecture

Config-based flags and staged rollouts for low-risk releases

Git workflow consulting

Migration from Git Flow to short-lived branches, tailored to team size and CI maturity

10. Summary

Trunk-based development solves the core problem of classic branch models: the longer a branch exists in isolation, the more expensive integration becomes. Short-lived branches, lasting hours up to at most two days, keep the space for conflicts small. Feature flags decouple deployment from release and allow unfinished code to be safely integrated into the trunk without making it visible to users. Both together, however, only work with a fast, reliable CI pipeline that checks the trunk for regressions on every push, and with testing discipline through pre-commit hooks and small, quickly reviewable pull requests.

No team should adopt trunk-based development as a mere matter of style. The preconditions, especially a resilient CI infrastructure and adequate test coverage, determine whether frequent integration leads to more stability or produces an unstable trunk. For Magento agencies with several developers working on the same codebase, a gradual transition, starting with the CI pipeline and ending with a lean feature flag infrastructure, is the most reliable path to faster, lower-risk releases.

Trunk-Based Development - The Essentials at a Glance

Short-lived branches

Hours to at most two days, merged into the trunk multiple times a day instead of rare, large integrations.

Feature flags

Decouple deployment from release. Unfinished code sits safely in the trunk but stays inactive for users.

Strong CI pipeline

Fast, automated tests on every push. A red build blocks every further merge.

Testing discipline & small PRs

Pre-commit hooks catch mistakes locally. Small pull requests stay fast and thorough to review.

11. FAQ: Trunk-Based Development

1What is trunk-based development?
A Git workflow with very short-lived branches integrated into the main branch several times a day, instead of maintaining long isolated feature branches.
2How long should a branch live at most?
A few hours, at most one or two days. Longer-lived branches drift away from the trunk and increase merge conflict risk.
3What is a feature flag and what is it for?
A code condition that turns a feature on or off. Decouples deployment from release, unfinished code stays safely inactive.
4Difference from Git Flow?
Git Flow uses several long-lived branch types with fixed release cycles. Trunk-based development reduces this to one trunk with feature flags instead of isolation.
5Difference from GitHub Flow?
GitHub Flow allows branches over several days until feature completion. Trunk-based development shortens that further and relies consistently on feature flags.
6What CI requirements are needed?
A fast, automated pipeline with static analysis and unit tests that runs reliably within a few minutes on every push.
7Suitable for small, inexperienced teams?
Not without qualification. Without a resilient CI infrastructure, such teams often benefit more from a bit more isolation, such as GitHub Flow.
8How do I migrate my team there?
First build the CI pipeline and test coverage, then gradually shorten branch lifetimes and introduce a simple feature flag infrastructure.
9How are releases handled?
Continuously straight from the trunk or via short-lived release branches. Feature flags additionally allow targeted activation independent of deployment.
10What happens to code reviews?
Reviews become smaller and faster. Many teams combine this with pair programming, so the review already happens while the code is being written.