ENG-45Recommended
Keep every flag in one register
List every feature flag in one file in the repository, with its type, owner, creation date and expiry or review date. A flag that is not in the register cannot be read by the code.
How to do it
Give each flag one of four types, because each lives for a different time. A release flag hides unfinished work and lives for weeks. An experiment flag runs an A/B test and ends with it. A kill switch turns off a feature under load, and a permission flag limits a feature to some customers. Kill switches and permission flags live for years, so give them a yearly review date instead of an expiry. Unleash, for example, marks release and experiment flags as stale after 40 days by default. Remove expired flags as Remove flags that never change says. Pick the flag tool with the feature flags pick.
Examples
JavaScript example
import { readFileSync } from 'node:fs';
const FIELDS = ['type', 'owner', 'created', 'expires'];
const register = JSON.parse(readFileSync('flags.json', 'utf8'));
const source = readFileSync('build/all-sources.txt', 'utf8');
const used = new Set([...source.matchAll(/isEnabled\('([a-z0-9-]+)'/g)].map(match => match[1]));
const problems = [
...[...used].filter(name => !register[name]).map(name => `${name} is not in the register`),
...Object.entries(register).filter(([, flag]) => FIELDS.some(field => !flag[field])).map(([name]) => `${name} lacks a field`),
...Object.keys(register).filter(name => !used.has(name)).map(name => `${name} is no longer read`),
];
if (problems.length) { console.error(problems.join('\n')); process.exit(1); }
Python example
import json, re, sys
from pathlib import Path
FIELDS = ["type", "owner", "created", "expires"]
register = json.loads(Path("flags.json").read_text())
source = Path("build/all-sources.txt").read_text()
used = set(re.findall(r"is_enabled\(\"([a-z0-9-]+)\"", source))
problems = [f"{name} is not in the register" for name in used - register.keys()]
problems += [f"{name} lacks a field" for name, flag in register.items() if any(not flag.get(f) for f in FIELDS)]
problems += [f"{name} is no longer read" for name in register.keys() - used]
if problems:
sys.exit("\n".join(problems))
How to check
A build step lists every flag name the code reads. It fails when one is missing from the register or lacks a type, owner, creation date or expiry. It also fails on a register entry the code no longer reads.
Related rules
- ARC-60 Remove flags that never change in Clean code and architecture
- STK-96 Use a settings file for flags and Unleash when flags need targeting in Software choices
- ENG-46 Hand flags to a new owner when people leave in Engineering
- ENG-49 Reusing an old flag for new behaviour in Engineering
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/engineering/feature-flags/flag-register">Agent Wranglers engineering playbook</a>. In code you write, add a comment next to it with the same link.