Skip to main content
Feature Flags: Deploy Daily, Release When Ready

Feature Flags: Deploy Daily, Release When Ready

Sep 7, 2025

You finished a feature. Its in main branch. But stakeholders arent ready to announce it. Or QA needs more time. Or you want to test with 1% of users first.

Feature flags let you deploy code without releasing it. The code is there, just hidden behind a boolean.

The Basic Idea

if (featureFlags.isEnabled('new-checkout-flow')) {
  return <NewCheckout />;
} else {
  return <OldCheckout />;
}

Simple but powerful. That one boolean controls whether users see new or old code.

Types of Feature Flags

Release flags: Hide incomplete features. Remove after launch.

Experiment flags: A/B tests. Remove after experiment concludes.

Ops flags: Kill switches. Keep permanently.

Permission flags: Control access to premium features.

Basic Implementation

class FeatureFlags {
  private flags: Map<string, FlagConfig> = new Map();

  isEnabled(flagName: string, context?: UserContext): boolean {
    const flag = this.flags.get(flagName);
    if (!flag) return false;

    // Global kill switch
    if (!flag.enabled) return false;

    // Percentage rollout
    if (flag.percentage < 100) {
      const bucket = this.getUserBucket(context?.userId);
      if (bucket > flag.percentage) return false;
    }

    // User allowlist
    if (flag.allowlist?.includes(context?.userId)) {
      return true;
    }

    return flag.enabled;
  }

  private getUserBucket(userId?: string): number {
    if (!userId) return Math.random() * 100;
    // Consistent hashing - same user always gets same bucket
    return hashCode(userId) % 100;
  }
}

Percentage Rollouts

Gradually expose features to more users:

// Start at 1%
await flags.update('new-search', { percentage: 1 });

// Monitor metrics, bump to 10%
await flags.update('new-search', { percentage: 10 });

// Looking good, go to 100%
await flags.update('new-search', { percentage: 100 });

// Remove flag entirely

The Kill Switch

When something goes wrong, turn it off instantly:

// In your admin panel or CLI
async function emergencyOff(flagName: string) {
  await flags.update(flagName, { enabled: false });
  await slack.notify(`🚨 ${flagName} disabled by kill switch`);
}

// In your monitoring
if (errorRate > threshold) {
  await emergencyOff('new-payment-flow');
}

No deployment needed. No waiting for CI. Just flip the switch.

Clean Up Your Flags

Old flags become tech debt. Track them:

const flags = {
  'new-checkout': {
    enabled: true,
    owner: 'payments-team',
    createdAt: '2024-01-15',
    expiresAt: '2024-03-15',  // Remind to clean up
    description: 'New checkout flow redesign'
  }
};

// Alert when flags are stale
function checkStaleFlags() {
  for (const [name, config] of Object.entries(flags)) {
    if (new Date(config.expiresAt) < new Date()) {
      alert(`Flag "${name}" has expired. Time to remove it!`);
    }
  }
}

Services To Consider

Rolling your own works for simple cases. For production:

Common Pitfalls

1. Testing both paths

// Always test with flag on AND off
describe('checkout', () => {
  it('works with new flow', () => {
    mockFlags({ 'new-checkout': true });
    // test new flow
  });

  it('works with old flow', () => {
    mockFlags({ 'new-checkout': false });
    // test old flow
  });
});

2. Flag nesting gets messy

// Avoid this nightmare
if (flags.isEnabled('featureA')) {
  if (flags.isEnabled('featureB')) {
    if (flags.isEnabled('featureC')) {
      // 8 possible combinations to test 😱
    }
  }
}

3. Forgetting to remove flags

Set calendar reminders. Add expiry dates. Run audits monthly.

Further Reading

Feature flags give you control. Deploy whenever. Release whenever. Roll back instantly. Just dont forget to clean up after yourself.

© 2026 Tawan. All rights reserved.