Best Practices
Follow these best practices to implement feature flags effectively and avoid common pitfalls. These guidelines will help you build a robust feature flag system that scales with your application.
Feature Flag Naming
Use Descriptive Names
// ✅ Good naming
'new-checkout-flow'
'beta-features'
'admin-panel-v2'
'pricing-page-redesign'
// ❌ Avoid
'flag1'
'new_feature'
'TEST_FLAG'
'feature-123'
Use Consistent Patterns
// Group related flags with prefixes
'checkout.new-flow'
'checkout.express-payment'
'checkout.guest-checkout'
// Use prefixes for different types
'exp.new-ui' // Experiment
'config.max-retries' // Configuration
'kill.emergency-disable' // Kill switch
'beta.new-features' // Beta features
Avoid Environment-Specific Names
// ❌ Don't include environment in flag names
'new-feature-dev'
'feature-prod'
'staging-flag'
// ✅ Use environment-agnostic names
'new-feature'
'feature-flag'
'experiment-flag'
Flag Lifecycle Management
Define Clear Lifecycles
// Flag lifecycle stages
const flagLifecycle = {
development: {
purpose: 'Local development and testing',
duration: '1-2 weeks',
cleanup: 'Remove after development',
},
testing: {
purpose: 'Internal testing and QA',
duration: '1-2 weeks',
cleanup: 'Remove after testing',
},
rollout: {
purpose: 'Gradual production rollout',
duration: '1-4 weeks',
cleanup: 'Remove after full rollout',
},
permanent: {
purpose: 'Long-term configuration',
duration: 'Ongoing',
cleanup: 'Review quarterly',
},
}
Set Expiration Dates
// Always set expiration dates for temporary flags
const flagConfig = {
name: 'new-checkout',
expirationDate: '2024-03-01',
reminderDate: '2024-02-15',
owner: '[email protected]',
}
Regular Cleanup
// Automated cleanup process
const cleanupFlags = async () => {
const flags = await df.getAllFeatures()
const expiredFlags = flags.filter(
flag => new Date(flag.expirationDate) < new Date(),
)
for (const flag of expiredFlags) {
console.log(`Flag ${flag.key} has expired`)
// Send reminder to owner
await sendCleanupReminder(flag)
}
}
Targeting Best Practices
Use Specific Targeting
// ✅ Specific targeting
const specificTargeting = {
rules: [
{
attribute: 'user.plan',
operator: 'equals',
value: 'premium',
},
{
attribute: 'user.country',
operator: 'in',
value: ['US', 'CA'],
},
],
}
// ❌ Too broad targeting
const broadTargeting = {
rules: [
{
attribute: 'user.email',
operator: 'contains',
value: '@',
},
],
}
Test Targeting Rules
// Test targeting with different user contexts
const testTargeting = async (featureKey, targetingRules) => {
const testUsers = [
{ userId: '1', plan: 'premium', country: 'US' },
{ userId: '2', plan: 'free', country: 'CA' },
{ userId: '3', plan: 'premium', country: 'UK' },
]
for (const user of testUsers) {
const result = await df.evaluateTargeting(featureKey, user, targetingRules)
console.log(`User ${user.userId}: ${result.enabled}`)
}
}
Gradual Rollouts
// Start with small percentages
const gradualRollout = {
phase1: { percentage: 1, duration: '1 day' },
phase2: { percentage: 5, duration: '3 days' },
phase3: { percentage: 25, duration: '1 week' },
phase4: { percentage: 50, duration: '1 week' },
phase5: { percentage: 100, duration: '1 week' },
}
Performance Optimization
Efficient Flag Evaluation
// Use batch evaluation for multiple flags
const evaluateMultipleFlags = async user => {
const flags = await df.evaluateMultiple(
['feature-a', 'feature-b', 'feature-c'],
user,
)
return flags
}
// Cache flag results appropriately
const cachedEvaluation = (() => {
const cache = new Map()
return async (featureKey, user) => {
const cacheKey = `${featureKey}-${user.userId}`
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
const result = await df.isFeatureEnabled(featureKey, user)
cache.set(cacheKey, result)
// Clear cache after 5 minutes
setTimeout(() => cache.delete(cacheKey), 300000)
return result
}
})()
Minimize Network Calls
// Initialize SDK with proper caching
const df = new DarkFeature('your-key', {
cacheTimeout: 300000, // 5 minutes
maxCacheSize: 1000,
enableBackgroundSync: true,
})
// Wait for initialization
await df.waitForInitialization()
Security Considerations
Client-Side vs Server-Side
// Client-side (public key)
const clientDf = new DarkFeature('public-client-key')
// Can only read flags, cannot modify
// Server-side (private key)
const serverDf = new DarkFeature('private-server-key')
// Full access to all operations
User Context Security
// ✅ Safe user context
const safeUserContext = {
userId: '123',
email: '[email protected]',
plan: 'premium',
country: 'US',
}
// ❌ Don't include sensitive data
const unsafeUserContext = {
userId: '123',
email: '[email protected]',
password: 'secret', // Never include passwords
creditCard: '1234-5678-9012-3456', // Never include PII
ssn: '123-45-6789', // Never include sensitive data
}
Environment Isolation
// Ensure environment isolation
const ensureEnvironmentIsolation = () => {
const currentEnv = process.env.NODE_ENV
if (currentEnv === 'production') {
// Never enable debug features in production
if (process.env.DEBUG_MODE === 'true') {
throw new Error('Debug mode cannot be enabled in production')
}
}
}
Code Organization
Centralized Flag Management
// Create a centralized flag service
class FeatureFlagService {
constructor() {
this.df = new DarkFeature('your-key')
}
async isFeatureEnabled(featureKey, user, defaultValue = false) {
try {
return await this.df.isFeatureEnabled(featureKey, user)
} catch (error) {
console.error(`Error evaluating flag ${featureKey}:`, error)
return defaultValue
}
}
async getFeatureVariation(featureKey, user, defaultValue = null) {
try {
return await this.df.getFeatureVariation(featureKey, user)
} catch (error) {
console.error(`Error getting variation for ${featureKey}:`, error)
return defaultValue
}
}
}
// Use the service
const featureFlags = new FeatureFlagService()
const isEnabled = await featureFlags.isFeatureEnabled('new-feature', user)
Flag Constants
// Define flag constants
const FEATURE_FLAGS = {
NEW_CHECKOUT: 'new-checkout-flow',
BETA_FEATURES: 'beta-features',
ADMIN_PANEL_V2: 'admin-panel-v2',
PRICING_TEST: 'pricing-page-test'
} as const
// Use constants
const isEnabled = await df.isFeatureEnabled(FEATURE_FLAGS.NEW_CHECKOUT, user)
Type Safety
// TypeScript definitions
interface FeatureFlag {
key: string
enabled: boolean
defaultValue: boolean
targeting?: TargetingRules
}
interface User {
userId: string
email: string
plan: string
country: string
custom?: Record<string, any>
}
// Type-safe flag evaluation
async function isFeatureEnabled(
featureKey: keyof typeof FEATURE_FLAGS,
user: User,
): Promise<boolean> {
return await df.isFeatureEnabled(FEATURE_FLAGS[featureKey], user)
}
Testing Strategies
Unit Testing
// Test flag evaluation logic
describe('Feature Flag Evaluation', () => {
it('should enable feature for premium users', async () => {
const user = {
userId: '123',
plan: 'premium',
country: 'US',
}
const isEnabled = await df.isFeatureEnabled('premium-feature', user)
expect(isEnabled).toBe(true)
})
it('should disable feature for free users', async () => {
const user = {
userId: '456',
plan: 'free',
country: 'US',
}
const isEnabled = await df.isFeatureEnabled('premium-feature', user)
expect(isEnabled).toBe(false)
})
})
Integration Testing
// Test with real SDK
describe('DarkFeature SDK Integration', () => {
let df
beforeEach(async () => {
df = new DarkFeature('test-key')
await df.waitForInitialization()
})
it('should evaluate flags correctly', async () => {
const user = { userId: 'test-user' }
const result = await df.isFeatureEnabled('test-flag', user)
expect(typeof result).toBe('boolean')
})
})
End-to-End Testing
// Test complete user flows
describe('Feature Flag User Flows', () => {
it('should show new checkout for enabled users', async () => {
// Setup user with feature enabled
const user = createUserWithFeature('new-checkout', true)
// Navigate to checkout
await page.goto('/checkout')
// Verify new checkout is shown
await expect(page.locator('.new-checkout')).toBeVisible()
})
})
Monitoring and Alerting
Flag Usage Monitoring
// Monitor flag usage
const monitorFlagUsage = async () => {
const flags = await df.getAllFeatures()
for (const flag of flags) {
const usage = await df.getFlagUsage(flag.key)
if (usage.evaluationCount === 0) {
console.warn(`Flag ${flag.key} has not been evaluated`)
}
if (usage.lastEvaluated > 7 * 24 * 60 * 60 * 1000) {
// 7 days
console.warn(`Flag ${flag.key} has not been evaluated recently`)
}
}
}
Performance Monitoring
// Monitor flag evaluation performance
const monitorPerformance = async () => {
const metrics = await df.getPerformanceMetrics()
if (metrics.averageEvaluationTime > 100) {
// 100ms
console.warn('Flag evaluation is taking too long')
}
if (metrics.errorRate > 0.01) {
// 1%
console.error('Flag evaluation error rate is high')
}
}
Alerting Setup
// Set up alerts for critical issues
const setupAlerts = async () => {
await df.setupAlert({
type: 'flag-not-evaluated',
threshold: 24 * 60 * 60 * 1000, // 24 hours
action: 'email',
})
await df.setupAlert({
type: 'high-error-rate',
threshold: 0.05, // 5%
action: 'slack',
})
}
Documentation
Flag Documentation
// Document each flag
const flagDocumentation = {
'new-checkout': {
description: 'New checkout flow with improved UX',
owner: '[email protected]',
created: '2024-01-15',
expectedDuration: '2 weeks',
businessCase: 'Improve conversion rate by 10%',
technicalDetails: 'Uses new React components',
rollbackPlan: 'Switch back to old checkout if issues arise',
},
}
Team Guidelines
// Team guidelines document
const teamGuidelines = {
naming: 'Use kebab-case for flag names',
targeting: 'Always test targeting rules before deployment',
cleanup: 'Set expiration dates for all temporary flags',
monitoring: 'Monitor flag usage and performance',
documentation: 'Document business case and technical details',
}
Common Anti-Patterns
Don’t Use Flags for Everything
// ❌ Don't use flags for everything
if (isFeatureEnabled('show-button')) {
return <Button />
}
// ✅ Use flags for meaningful features
if (isFeatureEnabled('new-checkout-flow')) {
return <NewCheckout />
} else {
return <OldCheckout />
}
Don’t Nest Flags Deeply
// ❌ Don't nest flags deeply
if (isFeatureEnabled('feature-a')) {
if (isFeatureEnabled('feature-b')) {
if (isFeatureEnabled('feature-c')) {
// Complex nested logic
}
}
}
// ✅ Use a single flag for complex features
if (isFeatureEnabled('advanced-feature-set')) {
// Handle all advanced features together
}
Don’t Forget Cleanup
// ❌ Don't leave flags forever
if (isFeatureEnabled('old-feature')) {
// This will never be cleaned up
}
// ✅ Set expiration and cleanup
const flagConfig = {
name: 'temporary-feature',
expirationDate: '2024-03-01',
cleanupPlan: 'Remove after full rollout',
}
Next Steps
- Feature Flags - Core feature flag concepts
- Client-Side SDKs - Implement in web applications
- Server-Side SDKs - Backend integration
- Mobile SDKs - Mobile app implementation
Last updated on