Back to Blog
Feature FlagsDeploymentDevOpsBest Practices

Feature Flags: Implementation and Best Practices

Deploy with confidence using feature flags. From basic toggles to gradual rollouts to A/B testing integration.

B
Bootspring Team
Engineering
November 20, 2023
6 min read

Feature flags decouple deployment from release. Ship code to production without exposing it to users, then gradually roll out when ready. Here's how to implement them effectively.

Why Feature Flags?#

Use cases: - Gradual rollouts (1% → 10% → 50% → 100%) - A/B testing - Kill switches for problematic features - Beta testing with specific users - Trunk-based development - Environment-specific features

Basic Implementation#

1// Simple feature flag service 2interface FeatureFlag { 3 name: string; 4 enabled: boolean; 5 percentage?: number; 6 allowedUsers?: string[]; 7 allowedGroups?: string[]; 8 metadata?: Record<string, any>; 9} 10 11class FeatureFlagService { 12 private flags: Map<string, FeatureFlag> = new Map(); 13 14 constructor(private storage: FlagStorage) {} 15 16 async load(): Promise<void> { 17 const flags = await this.storage.getAll(); 18 flags.forEach((flag) => this.flags.set(flag.name, flag)); 19 } 20 21 isEnabled( 22 flagName: string, 23 context?: { userId?: string; groups?: string[] } 24 ): boolean { 25 const flag = this.flags.get(flagName); 26 27 if (!flag) return false; 28 if (!flag.enabled) return false; 29 30 // Check user allowlist 31 if (flag.allowedUsers?.length && context?.userId) { 32 if (flag.allowedUsers.includes(context.userId)) { 33 return true; 34 } 35 } 36 37 // Check group allowlist 38 if (flag.allowedGroups?.length && context?.groups?.length) { 39 if (flag.allowedGroups.some((g) => context.groups!.includes(g))) { 40 return true; 41 } 42 } 43 44 // Check percentage rollout 45 if (flag.percentage !== undefined && context?.userId) { 46 const hash = this.hashUserId(context.userId, flagName); 47 return hash < flag.percentage; 48 } 49 50 // No targeting, just check enabled 51 return flag.enabled && !flag.allowedUsers?.length && !flag.allowedGroups?.length; 52 } 53 54 private hashUserId(userId: string, flagName: string): number { 55 const str = `${userId}:${flagName}`; 56 let hash = 0; 57 for (let i = 0; i < str.length; i++) { 58 hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0; 59 } 60 return Math.abs(hash) % 100; 61 } 62}

Database Schema#

1// schema.prisma 2model FeatureFlag { 3 id String @id @default(cuid()) 4 name String @unique 5 description String? 6 enabled Boolean @default(false) 7 percentage Int? // 0-100 for rollout 8 allowedUsers String[] // User IDs 9 allowedGroups String[] // Group names 10 metadata Json? 11 createdAt DateTime @default(now()) 12 updatedAt DateTime @updatedAt 13 createdBy String? 14} 15 16model FeatureFlagAudit { 17 id String @id @default(cuid()) 18 flagName String 19 action String // created, updated, deleted 20 oldValue Json? 21 newValue Json? 22 userId String 23 timestamp DateTime @default(now()) 24}

React Integration#

1// Feature flag context 2const FeatureFlagContext = createContext<{ 3 isEnabled: (flag: string) => boolean; 4 flags: Record<string, boolean>; 5}>({ 6 isEnabled: () => false, 7 flags: {}, 8}); 9 10export function FeatureFlagProvider({ 11 children, 12 userId, 13}: { 14 children: React.ReactNode; 15 userId?: string; 16}) { 17 const [flags, setFlags] = useState<Record<string, boolean>>({}); 18 19 useEffect(() => { 20 fetch(`/api/feature-flags?userId=${userId}`) 21 .then((res) => res.json()) 22 .then(setFlags); 23 }, [userId]); 24 25 const isEnabled = (flag: string) => flags[flag] ?? false; 26 27 return ( 28 <FeatureFlagContext.Provider value={{ isEnabled, flags }}> 29 {children} 30 </FeatureFlagContext.Provider> 31 ); 32} 33 34// Hook 35export function useFeatureFlag(flagName: string): boolean { 36 const { isEnabled } = useContext(FeatureFlagContext); 37 return isEnabled(flagName); 38} 39 40// Component 41export function Feature({ 42 flag, 43 children, 44 fallback = null, 45}: { 46 flag: string; 47 children: React.ReactNode; 48 fallback?: React.ReactNode; 49}) { 50 const enabled = useFeatureFlag(flag); 51 return enabled ? <>{children}</> : <>{fallback}</>; 52} 53 54// Usage 55function Dashboard() { 56 return ( 57 <div> 58 <Feature flag="new-dashboard"> 59 <NewDashboard /> 60 </Feature> 61 62 <Feature flag="analytics-v2" fallback={<AnalyticsV1 />}> 63 <AnalyticsV2 /> 64 </Feature> 65 </div> 66 ); 67}

Gradual Rollout#

1// Rollout controller 2class RolloutController { 3 async updateRollout( 4 flagName: string, 5 percentage: number 6 ): Promise<void> { 7 if (percentage < 0 || percentage > 100) { 8 throw new Error('Percentage must be 0-100'); 9 } 10 11 await prisma.featureFlag.update({ 12 where: { name: flagName }, 13 data: { percentage }, 14 }); 15 16 // Log audit 17 await this.logAudit(flagName, 'rollout_updated', { percentage }); 18 19 // Invalidate cache 20 await this.invalidateCache(flagName); 21 } 22 23 async gradualRollout( 24 flagName: string, 25 steps: number[], 26 intervalMinutes: number 27 ): Promise<void> { 28 for (const percentage of steps) { 29 await this.updateRollout(flagName, percentage); 30 logger.info(`Rolled out ${flagName} to ${percentage}%`); 31 32 // Monitor for errors 33 await this.waitAndMonitor(intervalMinutes, flagName); 34 35 const errorRate = await this.getErrorRate(flagName); 36 if (errorRate > 0.01) { 37 // > 1% error rate 38 logger.error(`High error rate for ${flagName}, rolling back`); 39 await this.updateRollout(flagName, 0); 40 throw new Error('Rollout aborted due to high error rate'); 41 } 42 } 43 } 44 45 private async waitAndMonitor( 46 minutes: number, 47 flagName: string 48 ): Promise<void> { 49 await new Promise((resolve) => setTimeout(resolve, minutes * 60 * 1000)); 50 } 51} 52 53// Usage 54await rolloutController.gradualRollout( 55 'new-checkout', 56 [1, 5, 10, 25, 50, 100], 57 30 // 30 minutes between steps 58);

A/B Testing Integration#

1// A/B test variant assignment 2interface ABTest { 3 name: string; 4 variants: { 5 name: string; 6 weight: number; // 0-100 7 }[]; 8} 9 10function assignVariant( 11 test: ABTest, 12 userId: string 13): string { 14 const hash = hashUserId(userId, test.name); 15 16 let cumulative = 0; 17 for (const variant of test.variants) { 18 cumulative += variant.weight; 19 if (hash < cumulative) { 20 return variant.name; 21 } 22 } 23 24 return test.variants[test.variants.length - 1].name; 25} 26 27// Track conversions 28async function trackConversion( 29 testName: string, 30 userId: string, 31 conversionType: string 32): Promise<void> { 33 const variant = assignVariant(getTest(testName), userId); 34 35 await analytics.track('ab_test_conversion', { 36 testName, 37 variant, 38 userId, 39 conversionType, 40 timestamp: new Date(), 41 }); 42} 43 44// Usage 45const variant = assignVariant( 46 { 47 name: 'checkout-button-color', 48 variants: [ 49 { name: 'blue', weight: 50 }, 50 { name: 'green', weight: 50 }, 51 ], 52 }, 53 userId 54); 55 56<Button color={variant}>Checkout</Button>

Admin Dashboard#

1// API routes for flag management 2app.get('/api/admin/flags', requireAdmin, async (req, res) => { 3 const flags = await prisma.featureFlag.findMany({ 4 orderBy: { name: 'asc' }, 5 }); 6 res.json(flags); 7}); 8 9app.put('/api/admin/flags/:name', requireAdmin, async (req, res) => { 10 const { name } = req.params; 11 const { enabled, percentage, allowedUsers, allowedGroups } = req.body; 12 13 const oldFlag = await prisma.featureFlag.findUnique({ 14 where: { name }, 15 }); 16 17 const newFlag = await prisma.featureFlag.update({ 18 where: { name }, 19 data: { enabled, percentage, allowedUsers, allowedGroups }, 20 }); 21 22 // Audit log 23 await prisma.featureFlagAudit.create({ 24 data: { 25 flagName: name, 26 action: 'updated', 27 oldValue: oldFlag, 28 newValue: newFlag, 29 userId: req.user.id, 30 }, 31 }); 32 33 // Invalidate cache 34 await redis.del(`flag:${name}`); 35 36 res.json(newFlag); 37});

Best Practices#

DO: ✓ Use consistent naming (kebab-case) ✓ Set expiration dates for temporary flags ✓ Clean up old flags regularly ✓ Audit all flag changes ✓ Monitor feature performance ✓ Use flags for gradual rollouts DON'T: ✗ Create flags without owners ✗ Leave flags in code indefinitely ✗ Use flags for configuration ✗ Nest too many flags ✗ Skip testing both branches

Flag Lifecycle#

1## Flag Lifecycle 2 31. **Create** - Define flag with owner 42. **Develop** - Implement feature behind flag 53. **Test** - QA both enabled/disabled states 64. **Deploy** - Ship to production (disabled) 75. **Rollout** - Gradually enable for users 86. **Monitor** - Watch metrics and errors 97. **Full Release** - Enable for everyone 108. **Cleanup** - Remove flag from code 119. **Archive** - Delete flag configuration

Conclusion#

Feature flags enable safe, gradual releases. Start simple with boolean toggles, add percentage rollouts for gradual releases, and integrate with monitoring for automatic rollbacks.

Clean up flags regularly—technical debt accumulates when flags live forever.

Share this article

Help spread the word about Bootspring