Trunk-Based Development Needs Feature Flags and a Deletion Policy
Trunk-based development moves fast only when releases stay decoupled from deployment. Feature flags make that possible, but without a deletion policy they become hidden product logic, technical debt, and a growing source of incidents. This post shows how to run both with discipline.
Nesqual Tech AI
The fastest teams are the ones that remove code, not keep it hidden
A 2026 incident pattern is still painfully common: a team ships 40 small trunk-based merges in a day, but one stale flag left behind for 11 months quietly routes 7% of traffic through an abandoned payment path. The release looked safe; the rollback looked clean; the real failure was that nobody knew the flag still existed. That is why trunk-based development needs feature flags, and feature flags need a deletion policy.
Feature flags are not a sidecar for convenience. In a trunk-based system, they are the control plane that lets you merge incomplete work, keep batch sizes small, and reduce merge conflict time from hours to minutes. But every flag you add creates a second source of truth. If you do not delete flags on a schedule, your codebase accumulates dead branches, untested paths, and hidden authorization logic that no one remembers to audit.
The rule is simple: if a flag can outlive the feature it protects, it will eventually become production logic.
Why trunk-based development depends on feature flags
Trunk-based development works because it compresses integration risk. Teams merge to main multiple times per day, often 10-30 merges per engineer per week, and keep the branch always releasable. Feature flags let you ship code before you ship behavior.
Flags turn incomplete work into safe merges
Suppose your platform team is building a new billing flow for enterprise customers. Without flags, engineers hold the branch for 2-3 weeks, then merge a large diff with 1,200 lines changed. In review, that usually means more rework, more conflicts, and a higher defect rate. With a flag, the team can merge the scaffolding on day 2, hide the path behind billing_new_checkout, and keep the trunk green while product, QA, and security validate the implementation incrementally.
A realistic outcome looks like this:
- average pull request size drops from 650 lines to 180 lines
- merge conflicts fall by 40-60%
- lead time from commit to production drops from 2-4 days to under 8 hours
- rollback scope shrinks because the code is already in production, just not active
Flags also protect trunk-based release discipline
Trunk-based development is not "merge anything and hope." It requires the trunk to stay deployable. Feature flags help you separate deploy from release, which is the only way large teams can ship daily without freezing the branch for every incomplete dependency.
A practical example is a payment provider migration. You can deploy the new Stripe flow, validate logs, compare authorization latency, and keep 100% of users on the old path until the new path passes synthetic and canary checks. If the new path adds 18 ms median latency and 120 ms p95 latency during the first hour, the flag gives you a fast revert without redeploying.
Why feature flags need a deletion policy, not just a naming convention
Feature flags are cheap to create and expensive to forget. By 2026, most engineering orgs have learned to track flags in a service or database, but many still treat cleanup as optional. That creates a slow leak in code quality and operational trust.
Old flags create hidden product branches
Every stale flag is a branch that never got merged back. Over time, that branch becomes embedded in authorization checks, pricing logic, UI rendering, and observability filters. The result is not just clutter; it is risk.
A stale flag can cause:
- dead code that bypasses security review
- conflicting metrics because dashboards split by obsolete variants
- test flakiness because old paths are still reachable in CI
- performance overhead from evaluating dozens of unused rules on every request
At one enterprise SaaS company, a flag evaluation library added only 2-4 microseconds per check. That sounds trivial until the service evaluates 18 flags per request at 8,000 requests per second. Suddenly, the system burns extra CPU cores just to preserve forgotten behavior.
Deletion policy is a lifecycle control, not a housekeeping task
A deletion policy says when a flag must die, who owns the cleanup, and what evidence is required before removal. Without that policy, teams rationalize every stale flag as "temporary." Six months later, temporary flags are still in production, and nobody wants to touch them because they are entangled with edge cases.
A good deletion policy answers three questions:
- When does the flag expire? Tie it to a release date, migration window, or experiment end date.
- Who owns removal? Assign a named engineer or team, not a broad group.
- What proves it is safe to delete? Require metrics, logs, or test coverage that show only one path remains active.
A practical deletion policy that works in real systems
You do not need ceremony. You need a policy that is visible, automated, and enforced in code review and CI.
Use three flag classes
Classify every flag at creation time:
- Release flag: temporary, used to decouple deploy from release
- Ops flag: used for runtime control, such as throttling or failover
- Experiment flag: used for A/B tests or progressive delivery
Each class gets a different expiration rule. Release flags should usually die in 7-30 days. Experiment flags should die when the experiment ends, often within 14-45 days. Ops flags can live longer, but they still need review every quarter.
Enforce ownership and expiry in the repository
A deletion policy is strongest when it lives beside the code. Store metadata in YAML, JSON, or annotations, then fail builds when a flag is overdue.
# flags/billing_new_checkout.yaml
name: billing_new_checkout
owner: team-payments
class: release
created_at: 2026-02-14
expires_at: 2026-03-14
cleanup_issue: PAY-4821
rollout: 0%
status: active
A CI check can block merges when expires_at is in the past or when the cleanup issue is closed but the flag still exists.
#!/usr/bin/env bash
set -euo pipefail
TODAY=$(date +%F)
for file in flags/*.yaml; do
EXPIRES=$(grep '^expires_at:' "$file" | awk '{print $2}')
STATUS=$(grep '^status:' "$file" | awk '{print $2}')
if [[ "$STATUS" == "active" && "$EXPIRES" < "$TODAY" ]]; then
echo "Expired flag found: $file"
exit 1
fi
done
Automate deletion with rollout thresholds
Deletion should be triggered by evidence, not vibes. A common rule is: once a release flag reaches 100% rollout for 7 consecutive days and error rate stays within 0.1% of baseline, the flag is eligible for deletion. For experiments, delete when the winner is promoted and the losing path has zero traffic for 48 hours.
A clean deletion workflow looks like this:
Code merged -> flag created -> rollout begins -> metrics stable -> flag reaches 100% -> cleanup issue opens -> CI enforces expiry -> code path removed -> config deleted
That sequence keeps trunk-based development honest. The code path should not survive longer than the feature itself.
How to instrument flags so deletion is measurable
If you cannot measure flag usage, you cannot delete it safely. Instrumentation is the difference between disciplined cleanup and guesswork.
Track evaluation counts and exposure rates
Every flag should emit:
- evaluation count per service and endpoint
- percentage of requests seeing each variant
- last-seen timestamp for each variant
- user, tenant, or account cohort if the flag is targeted
A mature platform can surface a simple rule: if a flag has not been evaluated in 30 days, it is a deletion candidate. If a variant has been at 0.00% exposure for 14 days, it is a removal candidate even if the flag object still exists.
Use observability to prove the old path is dead
Before deleting a flag, verify:
- no logs mention the old branch
- no traces route through the old handler
- no alert depends on the flag name
- no dashboard splits by the old variant
This is where many teams get burned. They delete the code but leave a metric label, then break an SLO chart that still expects variant=control. The fix is to treat flag cleanup as a cross-functional change, not a local refactor.
Example: progressive rollout with safe cleanup
A typical rollout for a new search ranking model might look like this:
- 1% internal users for 24 hours
- 10% of paid tenants for 48 hours
- 50% of all traffic for 72 hours
- 100% for 7 days
- delete flag and remove dead model switch
If p95 latency rises from 84 ms to 91 ms at 50%, you keep the flag but investigate. If the final 100% period stays within 2 ms of baseline and error rate remains below 0.05%, delete the flag immediately after the cleanup ticket is verified.
Common Pitfalls
Treating flags as permanent architecture
Teams often leave flags in place because they fear breaking something. That fear is valid, but the answer is not permanence. The answer is expiry plus verification. If a flag exists for more than one release cycle, ask whether it is really a release flag or just hidden product logic.
Mixing release flags with authorization logic
Do not use a feature flag as a security boundary unless it is designed and reviewed as one. A release flag should not decide who can access data, approve payments, or bypass compliance checks. That kind of logic belongs in policy enforcement, not rollout control.
Forgetting test coverage for both branches
If the old path never runs in CI, deletion becomes risky. Add tests that force each flag state, or use contract tests that validate the behavior before and after rollout. In one case, a team removed a flag only to discover that the "off" branch still powered a mobile API fallback used by 3% of Android clients.
Letting flag counts grow without a budget
Set a hard budget. For example, no service may have more than 20 active release flags, 10 active experiment flags, or 5 active ops flags without architecture review. That cap forces prioritization and keeps the deletion policy real.
A reference operating model for 2026
If you want trunk-based development to stay fast, make flags a first-class operational object. The most effective teams in 2026 do four things consistently:
- Merge small, often, and behind flags.
- Attach ownership and expiry at flag creation.
- Track exposure and last-seen usage automatically.
- Delete the flag as soon as the feature is fully rolled out or the experiment ends.
A simple governance model can be enough:
Engineering lead approves flag creation
Platform team enforces expiry metadata
CI blocks overdue flags
Observability confirms zero usage
Owner deletes code and config in the same change
That model keeps trunk-based development lean and prevents feature flags from turning into permanent forks.
Key Takeaways
- Use feature flags to keep trunk-based development fast, but treat every flag as temporary by default.
- Assign an owner, class, and expiry date when the flag is created.
- Automate checks in CI so overdue flags fail builds instead of lingering in production.
- Measure evaluation counts, last-seen timestamps, and rollout percentages so deletion is evidence-based.
- Keep release flags out of security and authorization logic.
- Set a hard budget for active flags per service and review it monthly.
Trunk-based development needs feature flags because they reduce merge risk and keep the trunk releasable. But feature flags need a deletion policy because every unremoved flag becomes hidden complexity, operational drag, and future incident material.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Written by
Nesqual Tech AI
Nesqual Tech
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI