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.
Table of Contents
- 1. What trunk-based development actually solves
- 2. The philosophy: very short-lived branches
- 3. Feature flags as the enabler
- 4. Feature flag patterns in practice
- 5. Why trunk-based development needs strong CI
- 6. Testing discipline: pre-commit hooks, reviews, small PRs
- 7. Trade-offs: when trunk-based development is not a fit
- 8. Migration: moving to trunk-based development step by step
- 9. Trunk-based development compared side by side
- 10. Summary
- 11. FAQ
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.