Trunk-based development asks every developer to merge into one shared branch several times a day. DORA’s description of the capability is specific about batch size: each developer divides their own work into small batches and merges that work into trunk at least once (and potentially several times) a day, on branches that last hours instead of weeks.
The obvious objection arrives immediately. Plenty of work takes longer than a day. The standard answer is a feature flag, and it is the right answer. Merge the unfinished code with the flag held off and nobody sees it until you decide they should.
Most write-ups stop there. What they skip is that the flag did not delete the risk that used to sit in the merge. It relocated it, and the place it moved to has none of the ceremony that merge day always had.
Worth noticing what DORA’s own page leaves out, too. It describes batch size and branch lifetime, and it never mentions feature flags at all. Flags are the mechanism teams reach for to hit that batch size. They are not part of the definition, and treating them as the whole answer is how the practice goes wrong.
Key Takeaways
- A flag stands in for the merge. Held off for three weeks, it is a long-lived branch with better version control.
- “Trunk is always releasable” is a claim about one flag configuration, usually the one where every new flag is off.
- A green pipeline proves the off path works. The on path, which is the code you are about to ship, needs its own run.
- Two flag states carry almost all the value in CI: the one production runs now, and the one it runs a single flip later.
- A flag hides code paths and cannot hide a schema change, which is why migrations need expand and contract underneath.
1. A flag replaces the merge, not the branch
A long-lived branch defers two different things at once, and separating them makes the rest of this tractable.
Integration is whether your code and everyone else’s have been reconciled. Release is whether a user can see the result. A branch defers both. Trunk-based development cancels the first deferral by making you merge constantly, and a feature flag supplies the second so you still choose when people see the work. That is the trade the trunk-based development glossary entry sets out in short form.
So the flag takes over from the merge. Nothing takes over from the branch, because trunk-based development removed the need for one, and that is where the difference shows up later.
Hold a flag off for three weeks and version control will tell you the code is integrated, because it is. Nothing runs it. No test exercises it in the configuration it will ship in, no colleague has hit it locally, and the strongest evidence available is that it compiled. Merge conflicts really are gone, which is worth having. The property that made long-lived branches dangerous in the first place, a large batch of unexercised change arriving in one go, is sitting on trunk untouched.
Flag count is the wrong thing to watch. Track how long any single flag has sat in a state nothing exercises.
2. “Releasable” is a claim about one flag state
Trunk-based development promises that trunk stays releasable. Ask which configuration and the promise gets narrower.
Ten independent boolean flags describe 1,024 possible states of your application. Your pipeline runs one. Almost always it is the state an unconfigured environment produces, where every new flag falls back to its code default, and the code default is off because that is the safe thing to write.
A green trunk therefore proves something real and small. The state production is already in still works. That is the state your users are in, so it matters.
It says nothing about the state you are about to move to.
That is the relocation. On a long-lived branch the risk piled up at the merge, where a fortnight of divergence had to be reconciled in one sitting, at a moment everyone already treated as dangerous. Flipping a flag changes which code path a million requests take. It happens through a toggle in a dashboard that gives no hint the same care applies.
3. Which flag states CI should actually run
Every combination is an unreachable target. Twelve flags is 4,096 states, most of which cannot occur, several of which are nonsense, and every one of which somebody would have to maintain.
Two states carry almost all the value.
Start with the configuration production has right now. Your existing suite covers this already, assuming the test environment mirrors production defaults instead of drifting from them.
Then run that same configuration with the single flag you are about to flip, flipped. Teams skip this second run, and it is the only one that speaks to the change actually being made.
The mechanics are the same either way. Every Featureflip SDK ships a test client you build from a map of fixed values, so there is no mocking framework involved, and the per-language shape is written up in testing code that uses feature flags. The argument for covering both sides at all, with the Slack outage that makes it concrete, is rule four in the feature flag best practices pillar.
What is specific to trunk-based development is where the second run lives. One CI job per open flag sounds thorough and rots fast, because nothing deletes the job when the flag goes. The convention that survives contact with a small team is duller: the pull request that introduces a flag introduces the on-path test in the same pull request, and that test dies with the flag because it sits in the same file as the code it covers.
Prerequisite flags collapse the space when work is genuinely multi-part. Gate the sub-flags behind one parent and the group has a single off state, because a child cannot serve anything except its off variation while the parent is off. The evaluator applies that before any of the children’s targeting rules run, so it holds as a property of the configuration rather than a convention your code has to remember.
4. The one thing a flag cannot hide
A flag gates a code path. A database migration is not a code path. By the time the flag decides anything the migration has already run, and it ran for everybody.
This is where trunk-based development quietly breaks, because the flag creates a feeling of safety that the schema does not share. Merge the migration and the flagged read path together, ship on Tuesday, flip on Thursday, and the two days in between hold a schema that only unreleased code fully understands.
The way out is expand and contract, which predates feature flags and pairs well with them.
Expand first, additive changes only. A new column, a new table, a new nullable field. This deploys with no flag at all, because adding something nobody reads is safe. Then write both shapes, keeping the old one authoritative. Backfill the history so the two agree about the past as well as the present. Only now does the flag earn its place, and what it gates is the read path, meaning which shape this request trusts. Flip it, watch, and revert if it goes badly, because both shapes are still being maintained and rolling back costs a toggle.
Contract last, and later than feels necessary. The old column goes after the flag is removed from the code, not after the flag is turned on.
That ordering exists because the two halves have wildly different undo costs. Every step before the destructive one is undone by a flag flip, which takes seconds and needs no coordination. The destructive step is undone by a restore, which takes an incident and several people. Sequencing the work this way front-loads everything a toggle can still reverse.
5. Granularity, and when the removal clock starts
One flag per releasable decision is the granularity that holds up.
Per commit is too fine and produces flags nobody can name. Go the other way, one flag for a whole epic, and you lose the ability to release parts of the work separately, which was the point. A single flag stretched over six weeks of change is the long-lived branch again in a different hat.
The useful test is whether you would ever want the two halves in different states in production. If yes, they are two flags. If no, one flag, and a naming convention is what keeps the resulting inventory legible to somebody reading it six months later.
Removal is measured from the merge, and turning the flag on does not reset it. A flag that has been on for everyone for a month is finished work with a conditional wrapped around it, and the conditional is the part that costs you from then on. Pete Hodgson’s framing of toggles as “inventory which comes with a carrying cost” is the right model here, and trunk-based development raises the rate at which that inventory arrives. A team merging several times a day creates flags several times a week. Without a removal habit that becomes a rising floor of permanent complexity, and clearing out dead feature flags becomes a scheduled project rather than routine maintenance.
6. Where Featureflip fits
Three things in the platform line up with the practice directly.
Every SDK ships a test client, so running the on path takes a map of flag values and no mocking framework. Prerequisites let a multi-part feature present one switch to the outside world while staying several flags underneath. Staleness detection surfaces flags that have sat in one state long enough to look finished, which gives the removal habit something to run on besides memory.
Pricing shape has a bearing on this too. Paid plans include unlimited flags at a flat monthly price, so a migration that needs eleven flags open for a fortnight bills the same as a quiet month. A platform that meters flags or evaluations puts a price on the exact behavior trunk-based development asks for, and teams respond to a meter by holding flags open longer and reusing them for work they were never created for. Read up on the three pricing meters before picking a platform, since whichever one you land on will shape how freely your team creates flags.
The shorter version
- Trunk-based development is about batch size and branch lifetime. Flags are the mechanism teams use to get there.
- A flag substitutes for the merge, so a flag held off for weeks reproduces the long-lived branch inside trunk.
- A green pipeline proves one flag configuration works, and it is usually the one production already runs.
- Run two states: what production runs now, and what it runs one flip later. Add the on-path test in the same pull request that adds the flag.
- Use expand and contract for schema work, and let the destructive step wait until the flag is out of the code.
- One flag per releasable decision, with the removal clock starting at merge.
Frequently asked questions
Do you need feature flags for trunk-based development?
Not strictly, and plenty of teams practice it without them by keeping every change small enough to finish inside a day. The moment a piece of work outgrows that, the choice is a branch that lives for a week or a flag that hides the incomplete path. Flags are how most teams keep the batch size down without blocking work that takes longer, which is why the two practices travel together even though DORA’s definition covers only the branching half.
How long should a feature flag live in trunk-based development?
Long enough to finish the work and validate it, and no longer. A release flag that is on for everyone with no plan to remove it has stopped being a flag and become a permanent conditional. Watch the age of the oldest flag sitting in a state nothing exercises, because that flag carries the same unexercised batch a long-lived branch would have carried.
How do you test both paths of a feature flag in CI?
Build a test client with fixed flag values and run your assertions against each side, which every Featureflip SDK supports without network access or mocking. The practical rule is to cover the configuration production runs today plus the configuration one flip away, rather than attempting every combination. Adding the on-path test in the same pull request as the flag keeps the coverage tied to the flag’s lifetime, so it disappears when the flag does.
How do database migrations work with trunk-based development?
Through expand and contract, because a flag gates code paths and cannot gate a migration that has already run. Ship the additive change unflagged, write both shapes while the old one stays authoritative, backfill, and put the flag on the read path only. Drop the old shape after the flag has been removed from the code, since every earlier step can be reversed with a toggle and that one can only be reversed from a backup.
Is trunk-based development the same as continuous integration?
They are tightly linked and not identical. Trunk-based development is the branching model, meaning small frequent commits to one shared branch in place of long-lived feature branches. Continuous integration is the practice of building and testing every one of those commits automatically. Trunk-based development is what makes continuous integration meaningful, because integrating constantly is only useful if something verifies the result each time.
Featureflip is a flat-priced feature flag platform with a test client in every SDK, prerequisite flags for multi-part work, and staleness detection so finished flags do not quietly become permanent. Paid plans carry unlimited flags, so the bill does not climb as your team creates more of them. Start on the free Solo plan without a credit card.