Feature flags: controlled rollout without architectural debt
Centralizing flag identifiers, evaluating at boundaries, and retiring flags so experiments do not become permanent branches.
Feature flags unlock progressive delivery, but undisciplined use produces nested conditionals, stale paths, and ambiguity between “experiment” and “configuration.” I treat flags like API surface: few names, explicit ownership, and scheduled retirement.
Single source of keys
export const FeatureFlag = {
NEW_CHECKOUT_FLOW: "new_checkout_flow",
BULK_EXPORT_ASYNC: "bulk_export_async",
} as const;
export type FeatureFlagName = (typeof FeatureFlag)[keyof typeof FeatureFlag];
export function isEnabled(
flags: Record<string, boolean | undefined>,
name: FeatureFlagName,
): boolean {
return flags[name] === true;
}
Constants eliminate typo drift between server and client and keep audits grep-friendly.
Evaluate at boundaries
Flags are read at route handlers, job entrypoints, or layout shells—not buried inside shared utilities—so behavior remains traceable.
Lifecycle
Every long-lived flag needs an exit plan: merge, kill, or promote to durable configuration with documentation.
Takeaway: Flags accelerate shipping when they reduce risk; they destroy velocity when they multiply code paths without accountability.