Feature flags and trunk-based development
Long-lived feature branches cause painful merges and big-bang releases. Feature flags let you merge to trunk continuously and decouple deploy from release. A guide to the workflow, the kinds of flags, and managing flag debt.
There is a common, painful rhythm to building software: you branch off master, work
on a feature for two or three weeks, and then face a merge that has drifted so far from
the mainline that integrating it is its own mini-project — followed by a big-bang
release where a fortnight of changes all go live at once and you hold your breath. The
alternative is trunk-based development with feature flags, and the two together
change how a team ships. Here is how the workflow fits together and how to run it
without making a mess.
The core idea: decouple deploy from release
The insight that unlocks everything is that “deploying code” and “releasing a feature to users” do not have to be the same event. A feature flag is a runtime switch that decides whether a piece of code is active:
if Flipper.enabled?(:new_checkout, current_user)
render "checkout/new_flow"
else
render "checkout/classic"
end
The new checkout code is deployed — it is on production, merged into master, shipped
with every release — but it is not released until you flip the flag on. This single
separation is what makes everything else possible: you can merge unfinished work safely
(it is dormant behind an off flag), turn a feature on for 1% of users and watch, and
turn it off instantly without a deploy if it misbehaves.
Trunk-based development
With flags in hand, the branching strategy simplifies dramatically. Instead of
long-lived feature branches, everyone integrates into master (the trunk) constantly —
small commits, merged daily, behind flags when not yet complete:
- Branches are short-lived — hours or a day, not weeks. You merge to trunk while the feature is still hidden behind an off flag.
- Merges stay trivial because nothing drifts far from the mainline. The agonising three-week merge simply stops existing.
- Integration problems surface immediately, when they are small, instead of all at once at the end.
This is the opposite of the heavyweight git-flow with its long-running develop, release, and feature branches. Trunk-based development trades that ceremony for a discipline: keep the trunk always releasable, and hide incomplete work behind flags rather than in branches. It is what makes genuine continuous integration — and eventually continuous deployment — possible.
The kinds of flags
Not all flags are the same, and conflating them is a common mistake. They have different lifetimes and owners:
- Release flags hide an in-progress feature until it is ready. They are temporary — once the feature is fully launched, the flag (and the old code path) should be deleted. This is the trunk-based-development workhorse.
- Ops flags (kill switches) let you disable an expensive or risky subsystem under load — turn off a heavy recommendation widget during a traffic spike. These are long-lived operational controls.
- Experiment flags route different users down different paths for A/B testing, with the split and the metrics wired up. Temporary, ending when the experiment concludes.
- Permission flags gate features by plan, role, or beta access. These are effectively permanent — they are part of your product’s entitlement model, not temporary scaffolding.
Knowing which kind a flag is tells you its expected lifetime and whether it is debt to
be cleaned up or a permanent part of the system. In Ruby, flipper and rollout are
the common libraries; both let you target flags by user, group, or percentage.
Percentage rollouts and canaries
The payoff feature is gradual rollout. Rather than flipping a feature on for everyone at once, you enable it for a slice of traffic and widen as your confidence grows:
Flipper.enable_percentage_of_actors(:new_checkout, 5) # 5% of users
# watch error rates and conversion, then:
Flipper.enable_percentage_of_actors(:new_checkout, 25)
Flipper.enable(:new_checkout) # everyone
This turns a release from a cliff-edge into a dial. If the new code spikes errors at 5%, you flip it back off — instantly, no deploy, no rollback — and 95% of users never saw a thing. Releasing to internal staff first, then beta users, then a percentage, then everyone, is the safest way to ship anything risky, and flags are what make it a runtime decision rather than a series of deploys.
Managing flag debt
Here is the discipline that separates feature flags as a superpower from feature flags
as a swamp: temporary flags must actually be removed. A codebase where every feature
ever built is still wrapped in an if flag_enabled? becomes a combinatorial nightmare —
nobody knows which paths are live, testing must cover both branches of dozens of dead
flags, and the conditional clutter makes the code unreadable. The rules we follow:
- Give every temporary flag an owner and an expiry expectation when you create it.
- Treat flag removal as part of “done”. A feature is not finished when it is released; it is finished when the flag and the old code path are deleted and only the new path remains.
- Audit flags periodically. A flag that has been at 100% for three months is debt — remove it and the dead branch behind it.
- Keep flag checks shallow. A flag should ideally gate at one or a few clear decision points, not be sprinkled through every layer, or removing it later becomes archaeology.
The mantra: a release flag is a loan, not a gift. It buys you safe, continuous delivery, and you pay it back by deleting it once the feature has fully landed.
Verdict
Feature flags plus trunk-based development are a genuinely better way to ship. By decoupling deploy from release, you escape the long-branch merge hell and the big-bang release, integrate continuously, roll features out gradually with a runtime dial, and gain an instant kill switch when something goes wrong. The price is discipline: know what kind each flag is, and treat temporary flags as debt to be repaid by deletion. Run it that way and your team ships smaller, safer, and more often — which, in the end, is the whole point of all the tooling we build around shipping software.