Skip to main content
Feature-flag governance tied to requirements: rollout, observability and cleanup

Feature-flag governance tied to requirements: rollout, observability and cleanup

Why your flag count keeps climbing even though nobody remembers what half of them do

Most teams don't have a feature-flag problem on the day they create a flag. They have one about four months later, when nobody can say whether enablenewcheckout_v2 is still live for 3% of traffic, fully rolled out, dead, or quietly breaking something in a region no one checks.

The reason is pretty simple: flags get created with a clear intent in someone's head, and then that intent evaporates. The code ships. The person who wrote it moves to another squad. The flag sits in the config, technically "on," and six months later a new engineer finds it, has no idea if it's safe to remove, and leaves it alone. That's how a healthy codebase ends up with 180 flags where maybe 40 are doing anything useful.

This post is narrowly about one fix: tying each flag back to a requirement record, so every flag has an owner, an acceptance condition, and a built-in expiry. Not a general flagging strategy — just the governance link that keeps flags from becoming permanent, undocumented branches in your runtime.

The actual failure pattern

Here's the sequence that plays out constantly, and it almost never looks like negligence in the moment.

A product manager writes a story: "Roll out the new address autocomplete to reduce checkout drop-off." An engineer wraps it in a flag because that's the responsible thing to do. The flag goes to 10%, then 50%, then 100% over two weeks. The metric moves. Everyone's happy. The story gets marked done.

Notice what didn't happen. Nobody recorded that the flag holding at 100% for 14 days was the actual completion condition. Nobody wrote down that once it's stable, the flag and the old code path should both be deleted. The story closed on "it works," not on "it's cleaned up."

Multiply that across a year. Flags that reached 100% never got removed because removal was never anyone's defined job. The flags that got abandoned at 10% — because the experiment was inconclusive and the team moved on — are even worse. They sit there holding a fraction of users on a half-built path, and the only record of why is a Slack thread that's long gone. The trigger is almost always the same: the flag's lifecycle lives in the flag tool, but the reason for the flag lives in the requirement. The two are never connected, so neither one can tell you when the flag is done.

A minimal flag-to-requirement schema

You don't need a heavyweight system. You need enough structure that a flag can't exist without pointing back at something that explains it. Here's the smallest schema that actually holds up:

FieldWhat it holdsWhy it matters
flag_keyThe technical flag nameThe join key to your flag tooling
requirement_idLink to the requirement/story that justifies the flagNo orphan flags — every flag traces to intent
ownerA person, not a teamTeams don't clean up flags; people do
flag_typerelease / experiment / ops-kill-switch / permissionDifferent types have different expiry rules
rollout_targetThe acceptance condition (e.g. "100% for 14 days, no regression")Defines done, not just working
createdat / expiresatAuthoring date + expected expiryMakes staleness detectable automatically
observability_refLink to the dashboard/alert watching this flagTies the flag to its signal

The one field people skip is flag_type, and it's the one that saves you later. A release flag and a permanent kill-switch have completely different lifespans. If you treat them the same, your cleanup automation will either nag you about flags that are supposed to live forever, or ignore flags that should've died months ago.

A kill-switch for payment processing isn't stale at 300 days — it's load-bearing. An experiment flag at 300 days is a problem. The schema has to know the difference, and the only way it knows is if someone tagged it at authoring time.

Authoring-time tags: stop the problem at the source

The cheapest place to enforce governance is the moment the flag is born. Every flag that's gone rogue was created without an expiry and without an owner — because the flag tool let it.

  1. A requirement_id it links back to
  2. An owner who is a named individual
  3. An expires_at date, even if it's a rough guess

That last one causes the most pushback. "I don't know when this'll be done." Fine — put 90 days as a default. The point of expires_at isn't precision. It's giving your automation something to measure against so a human gets pinged to decide, rather than the flag silently aging forever.

Use a 90-day default expiry so owners get a timely nudge to decide without blocking flag creation.

Teams that make the expiry field optional end up with 70–80% of flags having no expiry at all within a quarter. Teams that make it required but allow a default date end up with nearly every flag carrying one. The default is what makes it stick — people will accept a nudge, but they won't volunteer extra typing.

Rollout acceptance criteria that actually define "done"

The gap between "the flag is at 100%" and "the flag can be removed" is where most flag debt lives. You close that gap by writing the acceptance condition into the requirement, not the flag.

> Flag enablenewcheckout_v2 is considered complete when it has been at 100% rollout for 14 consecutive days, with no P1/P2 incidents linked to the new path and checkout conversion within ±1% of baseline or better. On meeting this, the flag and the legacy code path are scheduled for removal.

Three things make this work: it has a duration (14 days), a safety bar (no incidents), and a next action (schedule removal). Most teams write the first two and forget the third — which is exactly why cleanup never happens. The criteria describe success but not the consequence of success.

For experiment flags, the acceptance criterion is different, and people get this backwards often: an experiment flag's "done" condition is a decision, not a rollout level. "Experiment concludes after reaching statistical significance or 30 days, whichever comes first; outcome recorded in the requirement; losing variant removed within 7 days." An experiment with no end date isn't an experiment — it's a permanent fork in your runtime that nobody consciously chose.

Observability checks tied to the flag, not just the system

A flag changes behavior for a slice of users. If your dashboards only show aggregate numbers, a flag can be quietly hurting the 10% it's enabled for while the overall metric looks fine.

The governance link here is the observability_ref field — every flag should point at the specific signal watching its slice. Concretely, that means:

  1. A metric split by flag state, not just overall (conversion for flag-on vs flag-off users)
  2. An error-rate comparison between the two cohorts
  3. An alert that fires if the flagged cohort diverges beyond a threshold

A common version of this failure: a team ships a new search ranking behind a flag at 20%. Aggregate search usage is flat, so everyone assumes it's neutral. But the flagged cohort's click-through is down noticeably, and because nobody split the dashboard by flag state, it takes three weeks and a support spike to surface. If the flag record had carried a link to a cohort-split dashboard from day one, the divergence shows up on day two.

The point isn't to instrument every flag like it's a mission-critical system. A flag without any signal attached is a change you're shipping blind, and the requirement record is the natural place to demand that signal exists before rollout starts.

Rollback and cleanup cadence

Rollback and cleanup are two different jobs that teams tend to blur together.

Rollback is the fast path: the flagged behavior is bad, flip it off, you're back to the old path instantly. This is the whole reason flags are worth the overhead. The requirement's rollout criteria should explicitly state the rollback trigger — "if flagged cohort error rate exceeds 2× baseline, auto-disable" — so the decision is made before the incident, not during it.

Cleanup is the slow, boring path everyone skips: once a flag has served its purpose, the flag and the dead code path both get removed. This is where cadence matters, because cleanup never happens on its own. A workable rhythm:

  1. Weekly — automation lists flags whose expires_at has passed or whose acceptance criteria are met. Owners get pinged.
  2. Each flag pinged gets one of three dispositions — extend (new expiry + reason), remove (ticket created), or convert to permanent (change flag_type, which exempts it from future nags).
  3. Monthly — a short review of flags that have been extended more than twice. Repeated extensions are a signal the flag is becoming permanent by accident, and someone should make that call deliberately.
  4. On removal — the requirement record gets updated to reflect the flag is gone, closing the loop so the trace stays accurate.

The step people drop is #4. If you remove the flag but leave the requirement saying it's active, your traceability quietly rots — and stale links between requirements and the things they point at cause their own cascade of problems. If you're already fighting that kind of drift, the mechanics in detecting stale requirement links with small automation recipes apply almost directly here, since a dead flag reference is just another kind of broken link.

An automation recipe to detect stale flags

Here's the actual check, in plain terms. Run it daily or weekly against your flag config joined to your requirement records.

A flag is stale if any of these are true:

  1. expiresat has passed and flagtype is not ops-kill-switch or permission
  2. The linked requirement_id is closed/done, but the flag is still active
  3. flagtype is experiment and createdat is more than 30 days ago with no recorded outcome
  4. The flag is at 100% rollout and has been stable past its acceptance window, but no removal ticket exists
  5. The requirement_id points to a record that no longer exists (orphaned flag)

Below is a simple workflow diagram of the daily check and notification flow.

Process diagram

Each stale flag gets routed to its owner with the specific reason it tripped. The reason matters — "this flag expired 12 days ago" is actionable, "review your flags" is not. Generic flag-review emails get ignored for months; targeted, single-flag pings with a clear reason tend to get resolved within a day or two.

The join is the whole trick. A flag tool alone can tell you a flag is old. Only the link to the requirement can tell you the flag is old and its reason for existing is gone — which is the combination that makes removal safe. This is the same principle behind automating any requirement-to-artifact relationship: the value isn't in either record alone, it's in the link being checked continuously rather than by memory. If you've already wired up requirement-to-test links through CI hooks, adding a flag-to-requirement check to the same pipeline is a small extension, not a new system.

A real scenario

A mid-sized B2B SaaS team — around 25 engineers, single product — had accumulated roughly 140 feature flags over about two years. Nobody could confidently say how many were still needed. An audit before a planned platform migration found that close to 90 of them were either fully rolled out with the old code path still present, or stuck at partial rollout from experiments that had ended long ago.

The cleanup itself wasn't the hard part. The hard part was deciding which flags were safe to remove, because the reasoning behind each one was gone. For about 30 flags, they had to re-run analysis or ask around just to figure out whether anyone still depended on them. That investigation ate roughly two sprint-weeks of engineering time that produced zero new value — pure archaeology.

After that, they added two things: a required requirementid and expiresat at flag creation, and a weekly stale-flag check joined against their story records. Over the following quarter the active flag count dropped to around 55 and stabilized. New flags now carry their reason and expiry from birth, so the archaeology problem doesn't recur. The uncomfortable lesson wasn't about tooling — it was that the flags had always been documented somewhere, just never in a place that survived people leaving.

When this makes sense — and when it's overkill

This level of governance pays off when you have enough flags that no single person can hold them all in their head, or when flags routinely outlive the people who created them. For most teams that's somewhere north of 30–40 active flags, or any team with meaningful turnover between squads.

For a stable four-person team with a handful of flags, the full schema is probably more overhead than it's worth. You can clean up ten flags in an afternoon by just reading the code. Don't build governance for a problem you don't have yet.

  1. Who should not do this

    teams using flags as config toggles for truly permanent behavior — feature entitlements, tier gates, kill-switches. Those aren't rollout flags, and forcing expiry logic onto them creates noise that trains everyone to ignore the alerts. Tag them permission or ops-kill-switch, exempt them, and move on.

Who should not do this: teams using flags as config toggles for truly permanent behavior — feature entitlements, tier gates, kill-switches. Those aren't rollout flags, and forcing expiry logic onto them creates noise that trains everyone to ignore the alerts. Tag them permission or ops-kill-switch, exempt them, and move on.

A feature flag is a branch in your runtime, and like any branch, the cost isn't creating it — it's forgetting why it exists. Tying each flag to a requirement record with an owner, an acceptance condition, and an expiry doesn't make flags more work to create. It makes them possible to retire, which is the step that never happens on its own.

If flags are already piling up and you're not sure which ones are debt versus which are load-bearing, scoring them the way you'd score accumulated spec rot is a reasonable next move — the approach in the requirement debt register for detecting and remediating legacy-spec rot maps cleanly onto a flag inventory, since a stale flag is just debt wearing a config file as a costume.

Built for Product Teams Designed for agile workflows and collaborative requirement management
Save Time Eliminate manual tracking and reduce requirement ambiguity
Improve Quality Ensure alignment between stakeholders and development teams
Accelerate Delivery Streamline requirements handoffs and reduce project delays