Back to Blog
Feature FlagsDevOpsDeploymentTesting

Feature Flags: Progressive Rollouts and Safe Deployments

Implement feature flags for safer deployments. Learn patterns for progressive rollouts, A/B testing, and managing feature lifecycle.

B
Bootspring Team
Engineering
February 26, 2026
5 min read

Feature flags decouple deployment from release. This guide covers implementation patterns, best practices, and lifecycle management.

What Are Feature Flags?#

Feature flags are conditional statements that control feature visibility at runtime:

if (featureFlags.isEnabled('new-checkout')) { return <NewCheckout />; } return <LegacyCheckout />;

Types of Feature Flags#

Release Flags#

Control feature rollout:

1const flags = { 2 'new-dashboard': { 3 enabled: true, 4 rolloutPercentage: 25, // 25% of users 5 enabledForUsers: ['beta-testers'], 6 }, 7};

Experiment Flags#

A/B testing:

1const experiments = { 2 'checkout-button-color': { 3 variants: ['blue', 'green', 'orange'], 4 distribution: [0.33, 0.33, 0.34], 5 }, 6};

Operational Flags#

Kill switches:

const operationalFlags = { 'enable-caching': true, 'rate-limit-api': true, 'maintenance-mode': false, };

Implementation#

Basic Feature Flag Service#

1interface FeatureFlag { 2 key: string; 3 enabled: boolean; 4 rolloutPercentage?: number; 5 targetedUsers?: string[]; 6 targetedGroups?: string[]; 7} 8 9class FeatureFlagService { 10 private flags: Map<string, FeatureFlag>; 11 12 constructor(flags: FeatureFlag[]) { 13 this.flags = new Map(flags.map(f => [f.key, f])); 14 } 15 16 isEnabled(key: string, context?: UserContext): boolean { 17 const flag = this.flags.get(key); 18 if (!flag) return false; 19 if (!flag.enabled) return false; 20 21 // Check targeted users 22 if (flag.targetedUsers?.includes(context?.userId)) { 23 return true; 24 } 25 26 // Check targeted groups 27 if (flag.targetedGroups?.some(g => context?.groups.includes(g))) { 28 return true; 29 } 30 31 // Check rollout percentage 32 if (flag.rolloutPercentage !== undefined) { 33 const hash = this.hashUser(context?.userId || 'anonymous', key); 34 return hash < flag.rolloutPercentage; 35 } 36 37 return flag.enabled; 38 } 39 40 private hashUser(userId: string, flagKey: string): number { 41 const combined = `${userId}-${flagKey}`; 42 let hash = 0; 43 for (let i = 0; i < combined.length; i++) { 44 hash = ((hash << 5) - hash) + combined.charCodeAt(i); 45 hash = hash & hash; 46 } 47 return Math.abs(hash) % 100; 48 } 49}

React Integration#

1import { createContext, useContext, ReactNode } from 'react'; 2 3const FeatureFlagContext = createContext<FeatureFlagService | null>(null); 4 5export function FeatureFlagProvider({ 6 children, 7 service, 8}: { 9 children: ReactNode; 10 service: FeatureFlagService; 11}) { 12 return ( 13 <FeatureFlagContext.Provider value={service}> 14 {children} 15 </FeatureFlagContext.Provider> 16 ); 17} 18 19export function useFeatureFlag(key: string): boolean { 20 const service = useContext(FeatureFlagContext); 21 const user = useCurrentUser(); 22 23 if (!service) { 24 throw new Error('useFeatureFlag must be used within FeatureFlagProvider'); 25 } 26 27 return service.isEnabled(key, { 28 userId: user?.id, 29 groups: user?.groups || [], 30 }); 31} 32 33// Usage 34function Dashboard() { 35 const showNewWidget = useFeatureFlag('new-dashboard-widget'); 36 37 return ( 38 <div> 39 {showNewWidget && <NewWidget />} 40 <LegacyContent /> 41 </div> 42 ); 43}

Server-Side Implementation#

1// middleware/featureFlags.ts 2import { NextRequest, NextResponse } from 'next/server'; 3 4export async function middleware(request: NextRequest) { 5 const flags = await fetchFeatureFlags(); 6 const userId = request.cookies.get('user-id')?.value; 7 8 // Add flags to request headers for downstream use 9 const response = NextResponse.next(); 10 response.headers.set('x-feature-flags', JSON.stringify( 11 evaluateFlags(flags, { userId }) 12 )); 13 14 return response; 15}

Progressive Rollout Strategy#

Staged Rollout#

1const rolloutStages = [ 2 { percentage: 1, duration: '1h', name: 'canary' }, 3 { percentage: 10, duration: '4h', name: 'early-adopters' }, 4 { percentage: 50, duration: '24h', name: 'half-rollout' }, 5 { percentage: 100, duration: null, name: 'full-release' }, 6]; 7 8async function advanceRollout(flagKey: string) { 9 const flag = await getFlag(flagKey); 10 const currentStage = rolloutStages.findIndex( 11 s => s.percentage === flag.rolloutPercentage 12 ); 13 14 if (currentStage < rolloutStages.length - 1) { 15 const nextStage = rolloutStages[currentStage + 1]; 16 17 // Check error rates before advancing 18 const errorRate = await getErrorRate(flagKey); 19 if (errorRate > 0.01) { // 1% threshold 20 await rollback(flagKey); 21 return; 22 } 23 24 await updateFlag(flagKey, { 25 rolloutPercentage: nextStage.percentage, 26 }); 27 } 28}

Monitoring Integration#

1function trackFeatureFlagUsage( 2 flagKey: string, 3 enabled: boolean, 4 context: UserContext 5) { 6 analytics.track('feature_flag_evaluated', { 7 flag: flagKey, 8 enabled, 9 userId: context.userId, 10 timestamp: Date.now(), 11 }); 12} 13 14// Track errors associated with flags 15function trackError(error: Error, activeFlags: string[]) { 16 errorTracking.captureException(error, { 17 tags: { 18 activeFeatureFlags: activeFlags.join(','), 19 }, 20 }); 21}

Flag Lifecycle Management#

Expiration and Cleanup#

1interface FeatureFlagWithMetadata extends FeatureFlag { 2 createdAt: Date; 3 expiresAt?: Date; 4 owner: string; 5 status: 'active' | 'deprecated' | 'archived'; 6} 7 8// Regular cleanup job 9async function cleanupExpiredFlags() { 10 const flags = await getAllFlags(); 11 const now = new Date(); 12 13 for (const flag of flags) { 14 if (flag.expiresAt && flag.expiresAt < now) { 15 // Notify owner before removal 16 await notifyOwner(flag.owner, `Flag "${flag.key}" has expired`); 17 18 // Check if flag is still in codebase 19 const inCodebase = await searchCodebase(flag.key); 20 if (!inCodebase) { 21 await archiveFlag(flag.key); 22 } 23 } 24 } 25}

Documentation#

1// flags.config.ts 2export const featureFlags = { 3 'new-checkout': { 4 description: 'New streamlined checkout experience', 5 owner: 'checkout-team', 6 jiraTicket: 'SHOP-1234', 7 createdAt: '2024-01-15', 8 expectedRemoval: '2024-03-01', 9 dependencies: ['new-payment-provider'], 10 }, 11} as const;

Best Practices#

1. Keep Flags Short-Lived#

// Flag should be removed once feature is stable // Bad: flags that exist for years // Good: remove within 2-4 weeks of full rollout

2. Use Descriptive Names#

1// Bad 2'flag-123' 3'test-feature' 4 5// Good 6'checkout-one-click-purchase' 7'dashboard-analytics-v2'

3. Default to Off for New Features#

const defaultFlagState = { enabled: false, // Safe default rolloutPercentage: 0, };

4. Test Both States#

1describe('Checkout', () => { 2 it('works with new checkout enabled', () => { 3 setFeatureFlag('new-checkout', true); 4 // test new checkout 5 }); 6 7 it('works with new checkout disabled', () => { 8 setFeatureFlag('new-checkout', false); 9 // test legacy checkout 10 }); 11});

Common Pitfalls#

  1. Flag explosion: Too many flags become unmanageable
  2. Stale flags: Old flags left in codebase
  3. Testing gaps: Not testing both flag states
  4. Performance impact: Evaluating too many flags per request

Conclusion#

Feature flags enable safer deployments and progressive rollouts. Implement a clear lifecycle from creation to removal, monitor flag impact, and maintain discipline around flag cleanup.

Share this article

Help spread the word about Bootspring