Feature flags example: a real .md file, start to finish
The clearest feature flags example is one file: the frontmatter that defines who sees what, and the prose that says why. Most tutorials show a boolean and a conditional, then stop. That skips the part that actually breaks in production: who owns the flag, when it’s safe to delete, and what happened when it ran.
This post follows one real flag, new-checkout-v2: the file on disk, the code that reads it, the CLI check that validates it, and the commit that rolls it back. Every step here is the actual artifact, not a description of one.
Key Takeaways
- A complete feature flag example is a Markdown file with YAML frontmatter (
owner,audience,variants,weights,metric) and prose below it (the brief, the rationale, the decision).dif validatechecks the file before it ships: weights sum to 100, the owner is a real email, and the surface it targets exists.- The SDK call is one line,
dif("new-checkout-v2"), and it returns the same variant for the same user every time, computed locally with no network call.- A flag and an A/B test are the same file.
new-checkout-v2starts as a 50/50 experiment and becomes a flag at 100% just by editing the weights.- Rolling back means editing a number to
0and merging, so the reason for the rollback lives in the git history instead of someone’s memory of a dashboard toggle.
The file: a feature flag example end to end
Here is the whole thing, one Markdown file at dif/flags/active/new-checkout-v2.md:
---
id: new-checkout-v2
owner: priya@acme.com
surface: checkout
status: active
created: 2026-09-02
audience:
attribute: device_type
values: [desktop, mobile]
variants: [control, variant_a]
weights:
control: 50
variant_a: 50
metric: completed_checkout
exclusion_group: checkout-copy
---
## Brief
Replace the three-step checkout with a single-page flow for returning
customers.
## Rationale
Support tickets name the three-step flow as the top checkout complaint for
three straight months. `variant_a` collapses shipping, payment, and review
onto one page.
## Decision
(filled in by `dif conclude`)
That is the complete example. The frontmatter is the configuration a machine reads. The prose below it is the context a human, or an agent, reads to understand why the flag exists at all.
What each frontmatter field does
Every field in that block has one job:
id: the string the SDK calls (dif("new-checkout-v2")). It has to match the filename.owner: a real email.dif validaterejects a flag with no owner, because an unowned flag is how a dashboard ends up with a dead one. Martin Fowler’s canonical piece on feature toggles makes the same point: the hard part of a flag is removing it, not adding it.surface: the named product area (checkout,home,pricing) this flag belongs to. Surfaces accumulate learnings over time.audience: which users are eligible. Here,device_typein[desktop, mobile]. The actual values arrive at runtime; nothing about a real user is committed to the file.variantsandweights: the buckets and their split. Weights have to total 100.metric: the outcome this flag or experiment is measured against.exclusion_group: ties this flag to any other test touching the same surface, so a user lands in at most one.
The prose half, Brief, Rationale, and Decision, is what a dashboard has no field for. A dashboard row can hold the weights; it can’t hold the reason the test exists or what happened when it ran.
Reading the flag in code
The SDK call is the same regardless of what the frontmatter says. One line reads the flag and returns a variant:
import { dif } from "@dif.sh/sdk";
const variant = dif("new-checkout-v2");
if (variant === "variant_a") {
return renderNewCheckout();
}
return renderLegacyCheckout();
Assignment is the first four bytes of SHA-256(salt || user_id), modulo 10,000, mapped onto the weights above. SHA-256 is the standard NIST hash function, the same one used for most of the signing and checksums you already rely on. Nothing is stored. The same user id produces the same variant on every request, on the server and in the browser, so there’s no flicker between renders and no evaluation call to a service. When checkout completes, record the outcome the flag is measured against:
dif.track("completed_checkout", { value: 49 });
Validating the file before it ships
dif validate runs this check in CI, the same way a type checker runs on every PR:
$ dif validate
✓ dif/flags/active/new-checkout-v2.md
weights sum to 100
owner priya@acme.com is a valid email
surface "checkout" exists
no orphaned dif() call sites
✓ 1 file validated, 0 errors
If the weights summed to 90, or checkout-cta-v2 (an existing test on the same surface) wasn’t in the same exclusion_group, this would fail and block the pull request. The clash gets caught in review, not in an incident report after two experiments on the same surface contaminated each other’s numbers.
The same file becomes an experiment, or a flag
A flag and an A/B test aren’t two features. They’re the same file at different weights. new-checkout-v2 above, split 50/50 against completed_checkout, is already an experiment: hold it at that split until the numbers answer the hypothesis in the Rationale section.
When variant_a wins, ramping it is an edit, not a migration:
weights:
- control: 50
- variant_a: 50
+ control: 0
+ variant_a: 100
Commit that, merge it, and the same file is now a fully ramped release flag. The component reading it on line one of the code block above doesn’t change, because the schema and the SDK call never changed either.
Rolling back, and why that costs more than a toggle
Rolling back is the same diff in reverse: edit the weight to 0 for the variant you want to kill, and merge. Rollback here is a commit and a deploy, not a dashboard toggle. If your build takes ten minutes, the worst-case kill for this flag takes ten minutes. For a payment path that needs a sub-second kill switch, dif is the wrong tool for that specific flag, and something else is the right call.
For most flags, where the failure mode is “some users keep seeing the old checkout a little longer,” that cost buys something a toggle doesn’t: the commit message says who rolled it back and why, permanently, in the same history as the code it gates.
When the test is done, dif conclude writes the outcome into the file itself and archives it:
$ dif conclude new-checkout-v2 --decision ship --learning "Single-page checkout lifted completed_checkout 2.1%. Shipped."
That line also lands in the checkout surface’s learning log, so the next person running dif new on that surface sees it before they propose the same test again.
FAQ
What does a feature flag file example look like? YAML frontmatter (owner, audience, variants, weights, metric) followed by Markdown prose for the brief, the rationale, and the decision. The frontmatter above, for new-checkout-v2, is a complete one.
Is a feature flag example the same as an A/B test example? In dif, yes. The file format, the bucketing math, and the SDK call are identical. The only difference is the weights and whether a metric and hypothesis are attached. Ramping an experiment to 100% is editing two numbers, not building a new flag.
How do you roll back a feature flag? Edit the losing variant’s weight to 0 and merge. There’s no separate rollback mechanism, so the record of who rolled it back and why is the commit, not someone’s memory of a dashboard click.
Do feature flags need a database to store assignments? No, not in this model. The variant is computed from SHA-256(salt || user_id) mod 10,000 every time it’s read. Nothing is stored, so there’s no assignment table to keep in sync with the flag’s current state.
Can I see a feature flag example in a real framework, like React? Yes. Feature flags in React walks through the same new-checkout-v2-style flag wired up with a provider and the useDif hook.
Try it with your own flag
The feature flags example above is one file, one SDK call, and one CLI check. The frontmatter defines who sees what; the prose says why; dif validate catches a broken weight or a missing owner before it merges.
Install the CLI and scaffold a real one:
npm install -g @dif.sh/cli
dif init
dif init writes a dif/ directory with a working example flag already in it, so the first file to read is a real one, not a tutorial stub. The source for the CLI and SDK is on GitHub if you want to see the validator or the bucketing code directly.
The feature flags page covers the model end to end, and feature flags in git covers why this lives in the repo at all.
The .md format reference documents every frontmatter field.