← Back to blog

The Double-Edged Sword of Feature Flags: When Good Intentions Go Bad

2025-07-15 2 min read
tech
feature-flags
javascript
software-development
feature-toggles

I'm a huge fan of Feature Flags (a.k.a. Feature Toggles). For the uninitiated, they're essentially conditional code switches that let you deploy new functionality _without_ immediately releasing it to users. This enables:

  • Safer deployments (dark launching, canary releases)
  • Testing in production
  • Instant kill switches for faulty features
  • Granular user targeting (by geo, account, etc.)

But here's the critical nuance: Feature flags shine brightest when they're short-lived. They're tactical tools, not permanent architecture. Unfortunately, I've seen teams use them recklessly — ignoring lifecycle management and operational risks.

Let's unpack the anti-patterns.

Red Flag 1: Treating Flags as Permanent Configuration

The Sin: Using flags for long-term customer-specific customizations (e.g., flag_customer_x_ui_flow). The Cost:

  • Tech debt balloons as stale flags accumulate
  • Flag sprawl makes codebases unpredictable (if flag_a && !flag_b && flag_c...)
  • Testing complexity explodes combinatorially

The Fix: → Convert these to database-backed "behavior configurations". Store settings like customer_x_ui_flow_enabled in your own DB. Benefits:

  • Ownership: No vendor lock-in or third-party outages
  • Auditability: Track changes via application-level logs
  • Performance: Avoid runtime HTTP calls to flag services

Red Flag 2: Ignoring Third-Party Failure Modes

The Sin: Assuming your flag service (LaunchDarkly, Split, etc.) will _always_ be available. The Disaster: When the flag service goes down:

  • Applications may crash (if SDKs time out)
  • Flags default unpredictably (e.g., rolling back _all_ features)
  • Users experience inconsistent behavior

The Fix: → Implement graceful degradation:

try {
  featureEnabled = await featureFlagClient.getFlag("new-checkout");
} catch (error) {
  // Default to safe behavior during outages
  featureEnabled = false;
}

→ Cache flag states locally to survive short outages

→ Test failure scenarios (disable your flag service in staging!)

Red Flag 3: Immortal Flags

The Sin: Flags that never die ("This is a temporary code!"). The Consequence:

  • Configuration drift: Flags decay as requirements change
  • Security risks: Forgotten flags expose unfinished features
  • Cognitive overhead: New engineers battle "flag archaeology"

A Better Mindset: At my last company, we called them "Rollout Flags" — a deliberate naming trick:

  1. Flags _must_ have a removal plan upfront (e.g., "Remove after 90 days")
  2. Flags were tied to rollout milestones (not abstract "features"):
  • flag_enable_checkout_phase_1 → Remove after 95% rollout
  • flag_migrate_legacy_billing → Remove after data migration

This forced us to:

  • Automate flag cleanup (e.g., CI fails if flags >60 days old)
  • Treat flags as transient scaffolding, not building blocks

My Golden Rules

  1. Short-Lived > Permanent: Flags are like surgical sutures — remove them once the wound heals.
  2. Own Your Config: Customer-specific behavior? Use your database, not a flag vendor.
  3. Plan for Failure: What happens when your flag service explodes?
  4. Name Intentionally: "Rollout Flags" > "Feature Flags" (semantics shape behavior!).

Feature flags are powerful — but with great power comes great technical debt. Prune aggressively, design defensively, and keep your flag garden tidy.