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:
- Use camelCase or kebab-case consistently
- Prefix by feature area (e.g.,
auth*,payment*,ui*) - Use descriptive names that explain what the flag controls
- Keep names under 50 characters
- 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: true4. 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