Skip to content
← All posts
5 min read Dawid Skłodowski

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.