← Back to the library
ArchitectureApplied · 7 min

Nobody knows which feature flags are safe to remove

“There are hundreds of flags, many at 100% for a year. Nobody remembers who created them, removing one feels risky, and some code paths are only reachable with combinations nobody tests.”

THE PRINCIPLE

A release flag isn't done when it hits 100%; it's done when the flag and the old path are deleted.

FIRST MOVES

  1. Classify every flag: release, experiment, operational, kill switch, or permission. Only kill switches and permission flags should be long-lived; release, experiment and most operational flags are temporary.
  2. Give each flag an owner and an expiry at creation, in one typed registry the code must go through.
  3. Find removal candidates: flags fully on or fully off for weeks, flags not evaluated recently, and flags your flag service marks stale or potentially stale (Unleash does this automatically once a flag outlives its type's expected lifetime, for example 40 days for release flags).
  4. Make expiry enforceable: a CI check that fails on overdue temporary flags (warn the owner a week ahead so it doesn't ambush an unrelated PR), or a removal ticket created with the flag.
  5. Remove in one PR: hard-code the winning value, delete the dead branch and its tests, then archive the flag in the service.

TOOLS: THEN → NOW

Ad-hoc if (config.x) checks and env varsA vendor-neutral evaluation API (OpenFeature) in front of any flag provider
Manual spreadsheet cleanupsFlag types with expected lifetimes and stale markers (for example Unleash), plus CI expiry checks

PATTERN SNAPSHOT

import { OpenFeature, type EvaluationContext } from "@openfeature/server-sdk";

export const flags = {
  "checkout-v2":        { type: "release",     owner: "payments", expires: "2026-12-15", fallback: false },
  "pricing-killswitch": { type: "kill-switch", owner: "pricing",  expires: null,         fallback: true },
} as const;
export const isOn = (key: keyof typeof flags, ctx: EvaluationContext) =>
  OpenFeature.getClient().getBooleanValue(key, flags[key].fallback, ctx);

// flags.test.ts: fails CI once a temporary flag outlives its expiry
test("no overdue flags", () => {
  const today = new Date().toISOString().slice(0, 10);
  const overdue = Object.entries(flags).filter(([, f]) => f.expires !== null && f.expires < today);
  expect(overdue).toEqual([]);
});

CLOSE THE AI. EXPLAIN THIS.

Which flag types are expected to be long-lived, and what has to happen besides switching a release flag to 100%?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed