DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Feature Flags: Ship Code Safely With Runtime Control

Feature Flags: Ship Code Safely With Runtime Control

Feature flags let you deploy code without releasing it. You merge to main, deploy to production, and turn the feature on for 1% of users — or just yourself — until you're confident.

This is how Netflix, Facebook, and every serious engineering team ships software.

The Core Pattern

A feature flag is just a conditional that reads from an external config:

// Without feature flags — risky
function processPayment(order: Order) {
  return newPaymentProcessor.charge(order); // Goes live for everyone immediately
}

// With feature flags — controlled rollout
async function processPayment(order: Order) {
  const useNewProcessor = await flags.isEnabled('new-payment-processor', {
    userId: order.userId,
  });

  if (useNewProcessor) {
    return newPaymentProcessor.charge(order);
  }
  return legacyPaymentProcessor.charge(order);
}
Enter fullscreen mode Exit fullscreen mode

Build vs Buy

Build it yourself (good for simple on/off flags):

// Simple flag store in Redis
class FeatureFlags {
  constructor(private redis: Redis) {}

  async isEnabled(flag: string, context: { userId?: string }): Promise<boolean> {
    // Check user-specific override first
    if (context.userId) {
      const userOverride = await this.redis.get(`flag:${flag}:user:${context.userId}`);
      if (userOverride !== null) return userOverride === '1';
    }

    // Fall back to global flag value
    const global = await this.redis.get(`flag:${flag}`);
    return global === '1';
  }

  async setFlag(flag: string, enabled: boolean): Promise<void> {
    await this.redis.set(`flag:${flag}`, enabled ? '1' : '0');
  }
}
Enter fullscreen mode Exit fullscreen mode

Use a service (LaunchDarkly, Unleash, Flagsmith) when you need:

  • Percentage rollouts (enable for 10% of users)
  • User segmentation (beta users only)
  • A/B testing with metrics
  • Kill switches that non-engineers can toggle

Percentage Rollout

For gradual rollouts, hash the user ID to get a consistent bucket:

function getUserBucket(userId: string): number {
  // Consistent hash — same user always gets same bucket
  let hash = 0;
  for (const char of userId) {
    hash = ((hash << 5) - hash) + char.charCodeAt(0);
    hash |= 0;
  }
  return Math.abs(hash) % 100;
}

async function isEnabled(flag: string, userId: string, rolloutPercent: number): Promise<boolean> {
  return getUserBucket(userId) < rolloutPercent;
}

// Enable for 5% of users
const enabled = await isEnabled('new-dashboard', user.id, 5);
Enter fullscreen mode Exit fullscreen mode

Testing With Flags

Feature flags can make testing tricky. Always make flags injectable in tests:

// Good: inject flags, easy to test
async function processOrder(order: Order, flags: FeatureFlags) {
  const useFastCheckout = await flags.isEnabled('fast-checkout');
  // ...
}

// In tests
const mockFlags = { isEnabled: async () => true };
await processOrder(testOrder, mockFlags);
Enter fullscreen mode Exit fullscreen mode

The Ship Fast Workflow

Feature flags are a core part of continuous deployment:

  1. Merge feature behind a flag (dark launch)
  2. Enable for internal team
  3. Enable for 1% → 10% → 50% → 100%
  4. Remove the flag after full rollout

This pattern — along with CI/CD, preview environments, and automated testing — is baked into the Ship Fast Skill Pack as a ready-to-use Claude Code workflow. Stop setting up infrastructure from scratch on every project.

Top comments (0)