Skip to content

Best Practices

Naming Conventions

Use consistent, descriptive flag names:

ts
// ✅ Good naming
{
  newCheckoutFlow: true,
  experimentDashboardRedesign: { enabled: true, variants: [...] },
  authSocialLogin: true,
  paymentApplePay: true,
  enableDarkMode: true
}

// ❌ Avoid
{
  flag1: true,
  temp: false,
  test: true,
  'new-feature!': true
}

Recommended Patterns:

  1. Use camelCase or kebab-case consistently
  2. Prefix by feature area (e.g., auth*, payment*, ui*)
  3. Use descriptive names that explain what the flag controls
  4. Keep names under 50 characters
  5. Use action verbs for boolean flags (e.g., enable*, show*, use*)

Flag Lifecycle

Feature flags should be temporary. Follow a structured lifecycle:

1. Introduction (0-10% rollout)

ts
newFeature: {
  enabled: true,
  variants: [
    { name: 'disabled', weight: 90, value: false },
    { name: 'enabled', weight: 10, value: true }
  ]
}

2. Gradual Increase (10-50% rollout)

ts
// Week 1: 10% → 25%
newFeature: {
  enabled: true,
  variants: [
    { name: 'disabled', weight: 75, value: false },
    { name: 'enabled', weight: 25, value: true }
  ]
}

3. Full Rollout (100%)

ts
// Simplify to boolean
newFeature: true

4. Cleanup (Remove flag)

After the feature has been stable at 100% for 2-4 weeks, remove the flag entirely:

ts
// Before: Feature behind flag
if (isEnabled('newFeature')) {
  return <NewComponent />
} else {
  return <OldComponent />
}

// After: Flag removed
return <NewComponent />

Performance Considerations

Per-Request Caching

Flags are automatically cached per request:

ts
// ✅ Efficient - flags are cached
export default defineEventHandler((event) => {
  const flags1 = getFeatureFlags(event)
  const flags2 = getFeatureFlags(event)
  // Both return the same cached instance
})

Minimize Flag Checks in Loops

ts
// ❌ Inefficient
for (let i = 0; i < items.length; i++) {
  if (isEnabled('newFeature')) {
    processItem(items[i])
  }
}

// ✅ Efficient
const useNewFeature = isEnabled('newFeature')
for (let i = 0; i < items.length; i++) {
  if (useNewFeature) {
    processItem(items[i])
  }
}

Use Computed Properties

ts
// ✅ Better - computed property caches the result
const showNewFeature = computed(() => isEnabled('newFeature'))

Testing Strategies

Test Both States

ts
describe('FeatureComponent', () => {
  it('shows new feature when flag is enabled', () => {
    // Mock enabled state
    vi.mock('#feature-flags/composables', () => ({
      useFeatureFlags: () => ({
        isEnabled: (flag: string) => flag === 'newFeature'
      })
    }))
    
    const wrapper = mount(FeatureComponent)
    expect(wrapper.find('.new-feature').exists()).toBe(true)
  })
  
  it('shows old feature when flag is disabled', () => {
    // Mock disabled state
    vi.mock('#feature-flags/composables', () => ({
      useFeatureFlags: () => ({
        isEnabled: () => false
      })
    }))
    
    const wrapper = mount(FeatureComponent)
    expect(wrapper.find('.old-feature').exists()).toBe(true)
  })
})

Test All Variants

ts
describe('CheckoutFlow with variants', () => {
  it('renders control variant correctly', () => {
    vi.mock('#feature-flags/composables', () => ({
      useFeatureFlags: () => ({
        getVariant: () => 'control'
      })
    }))
    
    const wrapper = mount(CheckoutFlow)
    expect(wrapper.find('.old-checkout').exists()).toBe(true)
  })
  
  it('renders treatment variant correctly', () => {
    vi.mock('#feature-flags/composables', () => ({
      useFeatureFlags: () => ({
        getVariant: () => 'treatment'
      })
    }))
    
    const wrapper = mount(CheckoutFlow)
    expect(wrapper.find('.new-checkout').exists()).toBe(true)
  })
})

Environment-Specific Configuration

ts
export default defineFeatureFlags(() => {
  const isDev = process.env.NODE_ENV === 'development'
  const isProd = process.env.NODE_ENV === 'production'
  
  return {
    // Always enabled in dev, controlled rollout in production
    newFeature: isDev ? true : {
      enabled: true,
      variants: [
        { name: 'disabled', weight: 70, value: false },
        { name: 'enabled', weight: 30, value: true }
      ]
    },
    
    // Debug features only in dev
    debugPanel: isDev,
    
    // Production-only features
    analytics: isProd
  }
})

Documentation

Keep a record of your flags:

ts
/**
 * Flag: newCheckoutFlow
 * Owner: @payments-team
 * Created: 2024-01-15
 * Purpose: Gradual rollout of redesigned checkout
 * Target: 100% by 2024-02-15
 * Removal: 2024-03-01
 * Metrics: Conversion rate, cart abandonment
 */
newCheckoutFlow: {
  enabled: true,
  variants: [...]
}

Cleanup Checklist

  • [ ] Feature at 100% rollout for at least 2 weeks
  • [ ] No critical issues reported
  • [ ] Metrics show stable or improved performance
  • [ ] Old code path no longer needed
  • [ ] Tests updated
  • [ ] Flag removed from configuration
  • [ ] All flag checks removed from codebase
  • [ ] Documentation updated

Released under the MIT License.