Skip to content

vt-c-feature-flags

Implement safe progressive rollouts using feature flags. Covers flag lifecycle, percentage rollouts, A/B testing, and kill switches for production safety.

Plugin: core-standards
Category: Operations
Command: /vt-c-feature-flags


Feature Flags Skill

Purpose: Enable safe, gradual feature releases with instant rollback capability. Feature flags decouple deployment from release, reducing deployment risk.

Why Feature Flags

Without feature flags: - Releases are all-or-nothing - Rollback requires redeployment - No way to test in production safely - Can't do A/B testing

With feature flags: - Release to 1% of users first - Instant kill switch for problems - Target specific users for testing - Measure impact before full rollout


Tool Recommendations

Tool Best For Pricing
LaunchDarkly Enterprise, complex targeting Per-seat
Unleash Self-hosted, open source Free (self-hosted)
Flipper (Ruby) Rails applications Free
Flagsmith Open source, hosted option Free tier
PostHog Feature flags + analytics Free tier

Implementation Patterns

Basic Flag Check

// ✅ GOOD - Clean flag check
if (featureFlags.isEnabled('new-checkout')) {
  return <NewCheckout />;
}
return <OldCheckout />;

Percentage Rollout

// LaunchDarkly example
const showNewFeature = ldClient.variation(
  'new-feature',
  { key: user.id },
  false // default
);

// Unleash example
const showNewFeature = unleash.isEnabled('new-feature', {
  userId: user.id,
});

User Targeting

// Target specific users (beta testers)
const context = {
  key: user.id,
  email: user.email,
  custom: {
    plan: user.plan,
    company: user.companyId,
  },
};

const showFeature = ldClient.variation('feature-x', context, false);

Flag Lifecycle

1. Creation

flag:
  key: new-checkout-flow
  name: "New Checkout Flow"
  description: "Redesigned checkout with fewer steps"
  type: boolean
  defaultValue: false
  tags: [checkout, ux, q4-release]

2. Development (Off)

  • Feature behind flag
  • Only enabled in development
  • Tests cover both paths

3. Internal Testing

  • Enable for team members
  • QA in staging environment

4. Beta Rollout

  • Enable for beta users (1-5%)
  • Monitor error rates and feedback

5. Progressive Rollout

Day 1: 1% of users
Day 2: 5% of users (if metrics good)
Day 3: 25% of users
Day 4: 50% of users
Day 5: 100% of users

6. Cleanup

  • Remove flag from code
  • Delete flag from dashboard
  • Critical: Don't leave stale flags!

Kill Switch Pattern

For critical features, implement instant disable:

// Kill switch - checked on every request
const isSystemHealthy = featureFlags.isEnabled('system-healthy');

if (!isSystemHealthy) {
  return <MaintenancePage />;
}

// Feature-specific kill switch
const checkoutEnabled = featureFlags.isEnabled('checkout-enabled');

if (!checkoutEnabled) {
  return <CheckoutUnavailable />;
}

A/B Testing Pattern

// Get variant instead of boolean
const variant = featureFlags.variation('pricing-page', user, 'control');

switch (variant) {
  case 'control':
    return <PricingPageA />;
  case 'variant-b':
    return <PricingPageB />;
  case 'variant-c':
    return <PricingPageC />;
  default:
    return <PricingPageA />;
}

// Track conversion
analytics.track('pricing-view', { variant });

Best Practices

DO

// ✅ Use meaningful flag names
'new-checkout-flow'
'enable-dark-mode'
'show-promo-banner-q4'

// ✅ Have defaults
const enabled = flags.get('feature', false);

// ✅ Test both states
describe('Feature X', () => {
  it('shows new UI when enabled', () => { ... });
  it('shows old UI when disabled', () => { ... });
});

// ✅ Clean up old flags
// TODO: Remove after 2024-03-01
if (flags.isEnabled('old-feature')) { ... }

DON'T

// ❌ Generic names
'feature-1'
'test-flag'

// ❌ No default (crashes if flag missing)
const enabled = flags.get('feature');

// ❌ Complex nested flags
if (flagA && (flagB || !flagC)) { ... }

// ❌ Business logic in flags
if (flags.get('price-' + product.id)) { ... }

Monitoring Flags

Track Flag Evaluations

// Log flag decisions for debugging
featureFlags.on('flag-evaluated', (key, value, context) => {
  analytics.track('flag-evaluation', {
    flag: key,
    value,
    userId: context.key,
  });
});

Alert on Flag Changes

# PagerDuty/Opsgenie rule
alert:
  name: "Kill switch activated"
  condition: "flag.checkout-enabled changed to false"
  action: "notify team"

Integration with Toolkit

Before Deployment

## Pre-Deploy Flag Checklist

- [ ] New feature behind flag?
- [ ] Flag default is OFF (safe)?
- [ ] Kill switch identified?
- [ ] Rollout plan documented?
- [ ] Both code paths tested?

Rollout Plan Template

## Rollout Plan: [Feature Name]

**Flag key:** `new-feature-x`
**Kill switch:** `feature-x-enabled`

### Rollout Schedule
| Day | Percentage | Duration | Success Criteria |
|-----|------------|----------|------------------|
| 1 | 1% | 24h | Error rate < 0.1% |
| 2 | 5% | 24h | No critical bugs |
| 3 | 25% | 48h | Metrics baseline |
| 4 | 100% | - | Full release |

### Rollback Triggers
- Error rate > 1%
- P95 latency > 2x baseline
- Support tickets spike

### Rollback Process
1. Disable flag in dashboard
2. Verify old behavior restored
3. Notify team
4. Investigate issue

Common Mistakes

1. Flag Sprawl

❌ 200 flags, nobody knows which are active
✅ Regular cleanup, max 30 active flags

2. No Cleanup Process

❌ Flags left in code forever
✅ Remove flag within 2 weeks of 100% rollout

3. Testing Only Happy Path

❌ Only test with flag ON
✅ Test both ON and OFF states

4. No Kill Switch

❌ No way to disable broken feature
✅ Every critical feature has kill switch