ScaledByDesign/Insights
ServicesPricingAboutContact
Book a Call
Scaled By Design

Fractional CTO + execution partner for revenue-critical systems.

Company

  • About
  • Services
  • Contact

Resources

  • Insights
  • Pricing
  • FAQ

Legal

  • Privacy Policy
  • Terms of Service

© 2026 ScaledByDesign. All rights reserved.

contact@scaledbydesign.com

On This Page

The Feature Flag GraveyardThe Feature Flag LifecycleThe Four Flag TypesThe Flag System ArchitectureLevel 1: Config File (Teams of 1-5)Level 2: Database-Backed (Teams of 5-15)Level 3: Feature Flag Service (Teams of 15+)The Cleanup SystemAutomated Expiration AlertsThe Cleanup ChecklistFlag Removal: [flag-name]The Quarterly Flag AuditThe RulesThe Payoff
  1. Insights
  2. Split Testing & Tracking
  3. Building a Feature Flag System That Doesn't Become Technical Debt

Building a Feature Flag System That Doesn't Become Technical Debt

January 29, 2026·ScaledByDesign·
feature-flagsdeploymentengineeringexperimentation

The Feature Flag Graveyard

Every codebase we audit has the same problem: hundreds of feature flags, most of them stale. Nobody knows which are still active. Nobody wants to remove them because "what if something breaks?" The flags that were supposed to reduce risk have become risk themselves.

// Real code we found in an audit
if (featureFlags.get("new_checkout_v2")) {        // Added 2023
  if (featureFlags.get("checkout_v2_fix")) {       // Added 2023
    if (featureFlags.get("checkout_v3_rollout")) { // Added 2024
      renderCheckoutV3();
    } else {
      renderCheckoutV2Fixed();
    }
  } else {
    renderCheckoutV2();
  }
} else {
  renderCheckoutV1(); // Nobody knows if this still works
}

Three layers of flags, four possible code paths, and the team is afraid to touch any of it. This is what happens when you add flags without a plan to remove them.

The Feature Flag Lifecycle

Every flag should have a lifecycle defined at creation:

interface FeatureFlag {
  // Identity
  id: string;
  name: string;
  description: string;
 
  // Lifecycle
  type: "release" | "experiment" | "ops" | "permission";
  createdAt: Date;
  createdBy: string;
  expiresAt: Date;          // When this flag should be removed
  owner: string;            // Who's responsible for cleanup
 
  // State
  status: "active" | "rolled-out" | "expired" | "archived";
  rolloutPercentage: number;
 
  // Targeting
  rules: TargetingRule[];
}

The Four Flag Types

TypePurposeTypical LifespanExample
ReleaseGradual rollout of new features1-4 weeksnew_search_ui
ExperimentA/B test variants2-8 weekspricing_page_test_q1
OpsKill switches, circuit breakersPermanentdisable_external_api
PermissionUser-level access controlPermanentbeta_access

Release and experiment flags are temporary. They must have an expiration date. Ops and permission flags are permanent but should be reviewed quarterly.

The Flag System Architecture

Level 1: Config File (Teams of 1-5)

// flags.ts — checked into version control
export const FLAGS = {
  "new-checkout": {
    type: "release",
    enabled: true,
    rollout: 100,        // Fully rolled out
    expiresAt: "2026-03-01",
    owner: "sarah",
    description: "New checkout flow with express payment",
  },
  "search-experiment": {
    type: "experiment",
    enabled: true,
    rollout: 50,
    expiresAt: "2026-02-28",
    owner: "mike",
    description: "Testing AI-powered search suggestions",
  },
  "disable-recommendations": {
    type: "ops",
    enabled: false,       // Kill switch — flip to true to disable
    owner: "platform-team",
    description: "Emergency kill switch for recommendation engine",
  },
} as const;

Pros: Simple, auditable, no external dependencies Cons: Requires deployment to change flags

Level 2: Database-Backed (Teams of 5-15)

// Flag evaluation with database-backed rules
async function evaluateFlag(
  flagId: string,
  context: FlagContext
): Promise<boolean> {
  const flag = await flagStore.get(flagId);
 
  if (!flag || !flag.enabled) return false;
  if (flag.expiresAt && new Date() > flag.expiresAt) return false;
 
  // Check targeting rules
  for (const rule of flag.rules) {
    if (matchesRule(rule, context)) {
      return rolloutCheck(flag.rollout, context.userId);
    }
  }
 
  return false;
}
 
// Deterministic rollout (same user always gets same result)
function rolloutCheck(percentage: number, userId: string): boolean {
  const hash = createHash("md5").update(userId).digest("hex");
  const bucket = parseInt(hash.substring(0, 8), 16) % 100;
  return bucket < percentage;
}

Level 3: Feature Flag Service (Teams of 15+)

Use LaunchDarkly, Unleash, Flagsmith, or similar. But still enforce lifecycle management.

The Cleanup System

This is the part everyone skips. Don't skip it.

Automated Expiration Alerts

// Run daily: find flags that should be cleaned up
async function auditFlags() {
  const flags = await flagStore.getAll();
  const now = new Date();
 
  for (const flag of flags) {
    // Flag is past its expiration date
    if (flag.expiresAt && now > flag.expiresAt) {
      await notify(flag.owner, {
        type: "flag_expired",
        message: `Flag "${flag.name}" expired on ${flag.expiresAt}. ` +
                 `Please remove the flag and clean up the code.`,
        flagId: flag.id,
      });
    }
 
    // Flag has been at 100% rollout for > 2 weeks
    if (flag.type === "release" && flag.rollout === 100) {
      const rolledOutAt = flag.lastModified;
      const twoWeeksAgo = new Date(now.getTime() - 14 * 86400000);
 
      if (rolledOutAt < twoWeeksAgo) {
        await notify(flag.owner, {
          type: "flag_stale",
          message: `Flag "${flag.name}" has been at 100% for 2+ weeks. ` +
                   `Time to remove the flag and make it permanent.`,
          flagId: flag.id,
        });
      }
    }
  }
}

The Cleanup Checklist

When removing a flag:

## Flag Removal: [flag-name]
 
- [ ] Verify the flag is at 100% rollout (or 0% if removing the feature)
- [ ] Remove all flag evaluation code
- [ ] Remove the "else" branch (old code path)
- [ ] Remove the flag from the flag store/config
- [ ] Update tests that reference the flag
- [ ] Deploy and verify no errors
- [ ] Delete any related experiment data (if experiment type)

The Quarterly Flag Audit

Every quarter, review ALL flags:

For each flag:
  1. Is it still needed? (If release flag at 100%, remove it)
  2. Is the owner still on the team? (Reassign if not)
  3. Is the expiration date still accurate? (Extend or remove)
  4. Is the code path for "flag off" still functional? (Test it)

Metrics to track:
  - Total active flags (target: < 20 for small teams)
  - Flags past expiration (target: 0)
  - Average flag age (target: < 30 days for release flags)
  - Flags without owners (target: 0)

The Rules

  1. Every release flag gets an expiration date at creation — no exceptions
  2. Every flag has an owner — when they leave, flags get reassigned
  3. Flags at 100% for 2+ weeks get removed — they're not flags anymore, they're dead code
  4. Maximum 20 active release/experiment flags — if you need more, clean up first
  5. Ops flags are reviewed quarterly — even permanent flags need attention
  6. Flag nesting is banned — if you need a flag inside a flag, your architecture has a problem

The Payoff

A well-managed feature flag system gives you:

  • Safe deployments — ship code behind flags, roll out gradually
  • Fast rollbacks — flip a flag instead of reverting a deployment
  • Clean experiments — test variants without deployment risk
  • Maintainable code — because stale flags get cleaned up automatically

The system that makes all of this possible isn't the flag evaluation engine — it's the lifecycle management. Build the cleanup into the system from day one, and feature flags will be the best tool in your engineering toolkit instead of the biggest source of technical debt.

Previous
AI Won't Fix Your Broken Data Pipeline
Next
Vibe Coding Is Destroying Your Codebase
Insights
A/B Testing Is Lying to You — Statistical Significance Isn't EnoughServer-Side Split Testing: Why Client-Side Tools Are Costing You RevenueThe Tracking Stack That Survives iOS, Ad Blockers, and Cookie DeathHow to Run Pricing Experiments Without Destroying TrustYour Conversion Rate Is a Vanity Metric — Here's What to Track InsteadBuilding a Feature Flag System That Doesn't Become Technical DebtThe Data Layer Architecture That Makes Every Test Trustworthy

Ready to Ship?

Let's talk about your engineering challenges and how we can help.

Book a Call