Key Takeaway
Trunk-based development means everyone commits to one main branch in short-lived branches merged within a day or two. Feature flags make it safe: they decouple deploy from release, so half-finished code ships dark behind a flag. Flags also become kill switches. The catch is hygiene — every flag needs an owner, an expiry, and a cleanup ticket.
Long-lived feature branches are where velocity goes to die. You branch off main, work for three weeks, and by the time you open the pull request the trunk has moved a thousand commits ahead of you. The merge is a knife fight. I have lost whole afternoons to conflicts that existed only because two branches drifted apart for too long — code that was individually correct but collectively incompatible.
Trunk-based development flips the model: integrate constantly, in tiny increments, so conflicts never grow larger than a few lines. But that raises an obvious question — if I merge unfinished work to main every day, how do I ship main to production without exposing a half-built feature? The answer is the feature flag, and the discipline around it is what separates teams who love trunk-based development from teams who quietly abandon it.
Deploy is not release, and treating them the same is the bug
The single most useful idea here is separating two events people usually blur together. Deploy is a technical act: the new binary is running on your servers. Release is a product act: real users can now see the feature. On a traditional branch workflow these happen at the same instant, which is why deploys feel scary — every push to production is also a launch. Feature flags pull them apart. You deploy code dozens of times a day, dark, while the release is a separate decision made later by flipping a flag, often by a product manager rather than an engineer.
Once deploy and release are separate, the whole risk profile changes. A deploy that only ships dormant code cannot break a user-facing feature, because none of that code runs yet. Rollback stops meaning revert and redeploy; it means flip the flag off — a config change that propagates in seconds instead of a build-and-ship cycle that takes minutes. That speed difference is the entire point.
// A release toggle wrapping an incomplete feature.
// This code is on main and deployed to prod — but dark.
if (await flags.isEnabled('checkout-v2', { userId: user.id })) {
return renderCheckoutV2(cart);
}
return renderCheckoutLegacy(cart); // still the default for everyoneShort-lived branches, small pull requests
The branch part of trunk-based development is simpler than people fear. You still use branches — you just keep them alive for hours or a day, not weeks. Cut a branch, make a focused change, open a small pull request, get it reviewed, merge. The trick that makes this work for large features is that you do not wait for the whole feature to be done. You merge a vertical slice behind a flag that is off, then the next slice, then the next. Main always builds, always passes tests, always deploys — the feature just is not visible until the flag says so.
- Keep branches under a day old — the longer a branch lives, the more it diverges from trunk and the worse the eventual merge.
- Merge behind an off flag rather than waiting for the feature to be complete; incomplete code on main is fine if no path reaches it.
- Require the main branch to stay releasable at every commit — a red trunk blocks the entire team, so protect it with CI gates.
- Prefer many small pull requests over one giant one; reviewers catch more in fifty lines than in five hundred.
Pair short-lived branches with a merge queue. It rebases and tests each pull request against the current tip of main before merging, so you never land a change that passed CI against a stale trunk but breaks against today's. This is what keeps main green when a dozen people integrate daily.
Not all flags are the same — know the four types
A common mistake is treating every flag identically. The taxonomy popularized by Pete Hodgson and adopted across the industry splits flags by how long they should live and how dynamic they need to be. A release toggle exists only to hide unfinished work and should die within days. An operational toggle — a kill switch — might live for months as a safety valve. Mixing these mental models is how you end up with a five-year-old release toggle nobody dares delete.
| Flag type | Purpose | Lifespan |
|---|---|---|
| Release toggle | Hide incomplete work merged to trunk | Days to weeks |
| Operational (kill switch) | Disable a subsystem fast during an incident | Months, long-lived |
| Experiment | A/B test — route cohorts to variants | Length of the experiment |
| Permission | Gate features by plan, role, or entitlement | Long-lived, part of the product |
Kill switches: the flag that earns its keep
The operational toggle is the one I would fight to keep even in a codebase allergic to flags. Wrap any risky dependency — a new payment provider, an expensive report query, a third-party API you do not fully trust — in a flag that defaults to on but can be flipped off instantly. When that dependency starts timing out at 2am, you do not wake up an engineer to write a revert. On-call flips the switch, the system sheds the failing path, and you debug in the morning. A kill switch turns a page-someone-now incident into a look-at-it-later one.
A flag is a runtime branch, which means both sides must actually work. Untested off-paths are a classic trap: the kill switch you flip during an incident routes traffic into code that has not run in production for months. Test both states in CI, and exercise your kill switches on a schedule so you know they still work before you need them.
Flag hygiene, or how the technique goes bad
Every feature flag is a conditional, and every conditional you never remove is permanent technical debt. A codebase with hundreds of stale flags becomes a combinatorial nightmare: the number of possible states doubles with each flag, and no test suite covers them all. LaunchDarkly and Unleash both build entire product surfaces around flag lifecycle precisely because teams are terrible at cleanup. The fix is not heroics — it is treating removal as part of finishing the feature, not a someday project.
- At creation, give every flag an owner and an expiry date — a flag with no owner is a flag nobody will ever remove.
- Once a release toggle hits 100 percent rollout and stays stable, open the cleanup ticket immediately — aim to remove it within about 30 days.
- Delete both the flag definition and the dead branch of code it guarded — a flag removed from the dashboard but left in code is worse, not better.
- Run a quarterly flag audit and let CI fail or warn on flags past their expiry so debt cannot silently accumulate.
On the tooling side, OpenFeature is worth knowing about. It is a CNCF incubating project — a vendor-neutral specification and set of SDKs that put a standard API in front of whatever flag backend you use, whether that is LaunchDarkly, Unleash, GO Feature Flag, or a self-hosted store. You write your code against the OpenFeature interface once, and swapping providers later becomes a config change rather than a rewrite. For a self-hosted setup, that portability is exactly the kind of lock-in insurance I want.
Adopt OpenFeature even if you start with a single provider. Coding against the vendor-neutral API from day one costs almost nothing and buys you the freedom to migrate flag backends later without touching business logic — which matters when today's pricing or self-hosting story changes.
Where I would start tomorrow
You do not need a platform to begin. Start with one release toggle on the next feature that would otherwise sit on a long branch — a boolean read from config is enough to prove the workflow. Add a kill switch to your riskiest dependency. Write down the owner and expiry in the pull request description. The moment the feature is fully live, delete the flag in the same sprint. Do that three or four times and trunk-based development stops being a scary methodology and becomes just how you work: main is always green, deploys are boring, and releases are a decision you make on purpose.

