Feature flag best practices that survive code review
Feature flag best practices that survive code review share one property: they are enforced by something other than a reviewer’s memory. A naming convention is a best practice until someone forgets it under deadline pressure. A required owner field that fails the build is enforced.
Most flag guidance stops at “give your flags good names.” The gap is the review itself. A flag can look fine in a diff and still have no naming convention, no owner, and no plan for when it comes out. A reviewer approves it anyway, because nothing in the PR makes those gaps visible.
This post covers the practices a reviewer can actually check in the few minutes they have for a PR: a naming pattern, an owner field, where the flag lives, a removal plan, and a check that runs before a human even opens the diff.
Key Takeaways
- A flag’s frontmatter should answer five questions before a reviewer reads the diff underneath it: what it does, who owns it, where it lives, how it splits, and how it comes out.
- Name the change, not the release:
checkout-one-click-buysurvives review;new-checkout-v2says nothing about what changed or when to remove it.dif validaterejects a flag with no validowneremail and fails the PR in CI, so the requirement is enforced by the build, not a reviewer’s memory.- The collision check (E007) catches two experiments landing on one surface in separate PRs, something no single diff can show a human reviewer.
- A flag with no stated removal condition is how a flag like
new-checkout-v2ends up stuck at 100% for three years with a dead branch behind it.
What a reviewer checks before approving a feature flag
A pull request reviewer has a few minutes, not an afternoon. Google’s own code review guidelines put the same constraint on any diff: the reviewer should be able to understand what a change does without asking the author first. A flag’s frontmatter is the part of the diff most likely to leave that question unanswered.
A reviewer approving a flag PR is approving five things: what it does, who owns it, where it lives, how the split behaves, and how it comes out. The frontmatter below answers the first four in under a minute:
---
id: checkout-one-click-buy
status: active
owner: checkout-team@example.com
surface: checkout
hypothesis: >
A one-click buy button lifts completed checkouts.
variants:
- id: control
weight: 90
- id: "on"
weight: 10
metrics:
primary: completed_checkout
created: 2026-09-14
---
Four fields, four questions answered (hypothesis and metrics.primary are there because the schema requires them). id says what changed. owner says who to ask. surface says where. variants says the current split. A reviewer reads this block before reading the code underneath it, because the config is the part most likely to be wrong. created, plus a stated removal condition, answers the fifth question: how long the flag sticks around. That’s next.
Feature flag naming conventions that survive review
Feature toggles are old enough that the canonical writeup on martinfowler.com already covers the implementation patterns. The naming convention below is specific to the review problem: a name a reviewer can trust without opening a second tab to ask what it means.
new-checkout-v2 tells a reviewer nothing except that there was an old one. checkout-one-click-buy tells them the surface and the change, and it still reads correctly in a year, when there is no v3 to compare it to.
Two rules do most of the work:
- Name the change, not the release.
checkout-one-click-buy, notcheckout-improvementsorcheckout-v2. - Drop the version suffix.
-v2says nothing durable. If a second attempt at the same idea needs its own file,checkout-one-click-buy-retrystill reads better than-v2.
The id becomes the dif("checkout-one-click-buy", branches) call site in the code, so the name is not just documentation. It shows up in the diff, in a grep, and in the dif build output.
Make the owner field a build-breaking requirement
An owner field that a validator checks behaves differently than one a style guide recommends. dif validate rejects a flag with no owner, or one that is not a real email, and a failing dif validate fails the PR in CI the same way a broken test does:
$ dif validate
error E003 dif/experiments/active/checkout-one-click-buy.md:1:1: `owner` is not a valid email: `checkout-team`
help: Use `name@example.com` format.
✗ 1 error(s), 0 warning(s)
That is the difference between “we ask people to fill in an owner” and an owner who is actually reachable six months later when someone needs to ask why checkout-one-click-buy is still at 50%. The reviewer does not have to remember to check for one. The build already checked.
Put the feature flag in the PR, not a dashboard
The practice underneath all the others is where the flag lives. A flag in a dashboard is reviewed by nobody; a click changes production, and the audit trail is whatever the dashboard’s own history page happened to keep. A flag as a Markdown file in the repo gets the same review as the code it gates: a diff, a comment thread, an approval, a merge.
In dif, the flag file sits next to the code it changes: dif/experiments/active/checkout-one-click-buy.md beside src/routes/checkout/+page.svelte. The reviewer who approves the button copy is the same person approving the flag that gates it, in the same pull request. It never becomes a second, unreviewed change to a dashboard nobody else watches. Feature flags in git covers the rest of that workflow, including how rollback becomes a commit instead of a dashboard toggle.
Write the removal plan before the flag ships
A flag with no removal plan becomes permanent infrastructure by accident. The common failure looks the same everywhere: a flag like new-checkout-v2 ramps to 100%, and the team moves on. Eighteen months later, nobody remembers if it is safe to delete, so nobody does. The dead code path and the flag both outlive the reason they existed.
The fix that survives review is a removal condition stated at ship time, not a cleanup sprint promised later:
- A ramp flag gets a target.
created: 2026-09-14, plus a stated condition: remove once it holds 100% for two weeks with no regression. - An ops flag (a kill switch) says it is permanent. Not every flag should be removed. A kill switch for a payment-provider outage is infrastructure, not debt, so the file should note it explicitly so a future reviewer doesn’t flag it as stale by mistake.
A reviewer who sees neither condition in the PR should ask for one before approving.
Validate feature flags before a human has to
Some checks are not reviewable by a human at all. Two experiments landing on the same surface in different PRs, a week apart, is invisible to either reviewer, since neither diff shows the other flag. dif validate scans the whole repo, not just the diff, and catches it:
$ dif validate
error E007 dif/experiments/active/checkout-cta-copy.md:1:1: experiments `checkout-cta-copy` and `checkout-one-click-buy` both target surface `checkout` without a shared exclusion_group, and their audiences are not provably disjoint
help: Add the same `exclusion_group:` value to both, narrow one of the audiences so they're provably disjoint, or change one of their `surface:` fields.
✗ 1 error(s), 0 warning(s)
It also flags the call site nobody removed: a dif("...") call with no matching active file gets warning W001. It does not check the reverse, a file with no call site. Make dif validate a required status check in branch protection, and a broken flag cannot merge. Validate feature flags in CI has the full GitHub Action.
It catches config errors, not intent errors. A flag can pass every check above and still gate the wrong code path. That call is still on the reviewer.
FAQ
What are feature flag best practices? Name each flag for the change it makes, give it an owner enforced by tooling, keep it in the same repo and PR as the code it gates, state a removal condition before it ships, and validate flags in CI so collisions and orphans are caught before a human has to find them.
How do you review a feature flag in a pull request? Check the same five things every time: what the flag does (the name), who owns it, where it lives (the surface), how the split behaves (the variants and weights), and how it comes out. If the frontmatter states all five, the review is mostly a read. If any are missing, ask for them before approving.
What is a good feature flag naming convention? Name the change, not the release: checkout-one-click-buy, not checkout-improvements or new-checkout-v2. A version-numbered name tells a reviewer there was a previous attempt and nothing else. A verb-object name still reads correctly a year later.
How long should a feature flag live before removal? As long as its stated purpose, and no longer. A ramp flag should have a target: remove it once it holds 100% for a set window with no regression. A kill switch is the exception, and the file should say so, so a future reviewer doesn’t flag it as stale by mistake.
Do feature flags need code review? Yes. A flag changes what code path runs in production, which makes it a production change like any other. A dashboard click gets none of the scrutiny a code diff gets. A flag reviewed as a file in a pull request gets the same scrutiny as the code it gates.
Should feature flags live in a dashboard or the codebase? In the codebase, next to the code they gate, if the goal is for them to be reviewed at all. A dashboard flag is disconnected from the PR that ships the feature, so nothing forces a second set of eyes on it. A flag as a file gets a diff, a reviewer, and a git history the dashboard does not have.
Getting started
Feature flag best practices that survive code review are the ones a reviewer does not have to remember to check for. Name the flag for the change, not the version. Make the owner field a requirement the build enforces, not a convention people forget under deadline. Keep the flag in the same repo and pull request as the code it gates. State a removal condition before the flag ships, not after. Let dif validate catch the collisions and orphans no single diff can show.
None of this needs a dashboard or a second system to keep in sync with the code. Install the CLI, scaffold a project, and the flag file becomes part of the same review your code already gets:
npm install -g @dif.sh/cli
dif init
See the feature flags landing page for the full model, or the .md format reference for every frontmatter field.