Back to Blog
Feature FlagsDeploymentDevOpsBest Practices

Feature Flags and Progressive Rollouts: Ship with Confidence

Master feature flags for safer deployments, A/B testing, and gradual rollouts. Reduce risk while shipping faster.

B
Bootspring Team
Engineering
February 5, 2026
7 min read

Feature flags transform how teams ship software. Instead of big-bang releases, deploy code whenever it's ready and control feature visibility separately. Bugs become manageable, experiments become easy, and deployments become boring (in a good way).

Why Feature Flags?#

Decouple Deployment from Release#

Traditional: Code Complete → Deploy → Feature Live (for everyone) With Feature Flags: Code Complete → Deploy → Flag Off → Gradual Rollout → Full Release

Benefits#

  • Reduce risk: Roll back features instantly without deploying
  • Test in production: Enable features for internal users first
  • A/B testing: Compare feature variants with real users
  • Kill switches: Disable problematic features immediately
  • Trunk-based development: Merge incomplete features safely

Implementing Feature Flags#

Basic Flag System#

1interface FeatureFlag { 2 key: string; 3 enabled: boolean; 4 rolloutPercentage?: number; 5 enabledFor?: string[]; // User IDs 6 rules?: FlagRule[]; 7} 8 9interface FlagRule { 10 attribute: string; 11 operator: 'eq' | 'in' | 'gt' | 'lt' | 'contains'; 12 value: unknown; 13 enabled: boolean; 14} 15 16class FeatureFlagService { 17 private flags: Map<string, FeatureFlag> = new Map(); 18 19 isEnabled(flagKey: string, context: FlagContext = {}): boolean { 20 const flag = this.flags.get(flagKey); 21 22 if (!flag) return false; 23 if (!flag.enabled) return false; 24 25 // Check user-specific enablement 26 if (flag.enabledFor?.includes(context.userId)) { 27 return true; 28 } 29 30 // Check rules 31 if (flag.rules) { 32 for (const rule of flag.rules) { 33 if (this.evaluateRule(rule, context)) { 34 return rule.enabled; 35 } 36 } 37 } 38 39 // Check rollout percentage 40 if (flag.rolloutPercentage !== undefined) { 41 return this.isInRollout(context.userId, flagKey, flag.rolloutPercentage); 42 } 43 44 return flag.enabled; 45 } 46 47 private isInRollout(userId: string, flagKey: string, percentage: number): boolean { 48 // Consistent hashing so same user always gets same result 49 const hash = this.hash(`${userId}:${flagKey}`); 50 return (hash % 100) < percentage; 51 } 52 53 private hash(str: string): number { 54 let hash = 0; 55 for (let i = 0; i < str.length; i++) { 56 hash = ((hash << 5) - hash) + str.charCodeAt(i); 57 hash = hash & hash; 58 } 59 return Math.abs(hash); 60 } 61}

Using Feature Flags#

1// Backend 2async function getProducts(userId: string) { 3 const products = await db.products.findMany(); 4 5 if (featureFlags.isEnabled('new-recommendation-engine', { userId })) { 6 return recommendationEngine.enhance(products, userId); 7 } 8 9 return products; 10} 11 12// Frontend (React) 13function ProductList() { 14 const { isEnabled } = useFeatureFlags(); 15 16 return ( 17 <div> 18 {isEnabled('new-product-card') ? ( 19 <NewProductCard /> 20 ) : ( 21 <LegacyProductCard /> 22 )} 23 </div> 24 ); 25}

Progressive Rollout Strategies#

Percentage-Based Rollout#

1const rolloutPlan = { 2 'new-checkout-flow': [ 3 { day: 1, percentage: 1, note: 'Internal testing' }, 4 { day: 3, percentage: 5, note: 'Early adopters' }, 5 { day: 5, percentage: 25, note: 'Quarter of users' }, 6 { day: 7, percentage: 50, note: 'Half of users' }, 7 { day: 10, percentage: 100, note: 'Full rollout' } 8 ] 9}; 10 11async function progressRollout(flagKey: string) { 12 const plan = rolloutPlan[flagKey]; 13 const currentDay = getDaysSinceStart(flagKey); 14 const stage = plan.findLast(s => s.day <= currentDay); 15 16 if (stage) { 17 await updateFlag(flagKey, { rolloutPercentage: stage.percentage }); 18 logger.info(`Rollout progressed: ${flagKey} now at ${stage.percentage}%`); 19 } 20}

Ring-Based Deployment#

1const rings = { 2 ring0: ['employee-ids'], // Internal employees 3 ring1: ['beta-user-ids'], // Beta users 4 ring2: { percentage: 10 }, // 10% of users 5 ring3: { percentage: 50 }, // 50% of users 6 ring4: { percentage: 100 } // Everyone 7}; 8 9function getRingForUser(userId: string, currentRing: number): boolean { 10 if (currentRing >= 0 && rings.ring0.includes(userId)) return true; 11 if (currentRing >= 1 && rings.ring1.includes(userId)) return true; 12 if (currentRing >= 2 && isInPercentage(userId, rings.ring2.percentage)) return true; 13 if (currentRing >= 3 && isInPercentage(userId, rings.ring3.percentage)) return true; 14 if (currentRing >= 4) return true; 15 return false; 16}

Attribute-Based Targeting#

1// Enable for specific user segments 2const flagConfig = { 3 key: 'premium-features', 4 rules: [ 5 { 6 attribute: 'subscription', 7 operator: 'eq', 8 value: 'premium', 9 enabled: true 10 }, 11 { 12 attribute: 'country', 13 operator: 'in', 14 value: ['US', 'CA', 'UK'], 15 enabled: true 16 }, 17 { 18 attribute: 'accountAge', 19 operator: 'gt', 20 value: 30, // days 21 enabled: true 22 } 23 ] 24};

Monitoring Rollouts#

Key Metrics to Track#

1// Track feature flag evaluations 2function trackFlagEvaluation(flagKey: string, enabled: boolean, context: FlagContext) { 3 metrics.increment('feature_flag_evaluation', { 4 flag: flagKey, 5 enabled: String(enabled), 6 variant: context.variant || 'default' 7 }); 8} 9 10// Compare metrics between flag states 11const dashboard = { 12 metrics: [ 13 'error_rate', 14 'response_time_p95', 15 'conversion_rate', 16 'user_engagement' 17 ], 18 comparisons: [ 19 { flag: 'new-checkout', enabled: true, metric: 'conversion_rate' }, 20 { flag: 'new-checkout', enabled: false, metric: 'conversion_rate' } 21 ] 22};

Automated Rollback#

1class RolloutGuard { 2 async checkHealth(flagKey: string): Promise<HealthStatus> { 3 const flaggedUsers = await this.getUsersWithFlag(flagKey, true); 4 const unflaggedUsers = await this.getUsersWithFlag(flagKey, false); 5 6 const metrics = { 7 flagged: await this.getMetrics(flaggedUsers), 8 unflagged: await this.getMetrics(unflaggedUsers) 9 }; 10 11 // Check if error rate is significantly higher for flagged users 12 if (metrics.flagged.errorRate > metrics.unflagged.errorRate * 1.5) { 13 return { 14 healthy: false, 15 reason: 'Error rate 50% higher for flagged users', 16 recommendation: 'rollback' 17 }; 18 } 19 20 return { healthy: true }; 21 } 22 23 async autoRollback(flagKey: string) { 24 const health = await this.checkHealth(flagKey); 25 26 if (!health.healthy) { 27 await featureFlags.disable(flagKey); 28 await alerting.send({ 29 severity: 'critical', 30 message: `Auto-rolled back ${flagKey}: ${health.reason}` 31 }); 32 } 33 } 34}

A/B Testing with Feature Flags#

Experiment Configuration#

1interface Experiment { 2 key: string; 3 variants: Variant[]; 4 traffic: number; // Percentage of users in experiment 5 startDate: Date; 6 endDate?: Date; 7} 8 9interface Variant { 10 key: string; 11 weight: number; // Relative weight for assignment 12 config?: Record<string, unknown>; 13} 14 15const experiment: Experiment = { 16 key: 'checkout-button-color', 17 traffic: 50, // 50% of users in experiment 18 variants: [ 19 { key: 'control', weight: 1 }, 20 { key: 'blue', weight: 1 }, 21 { key: 'green', weight: 1 } 22 ], 23 startDate: new Date('2024-01-15') 24};

Variant Assignment#

1function getVariant(experimentKey: string, userId: string): string | null { 2 const experiment = experiments.get(experimentKey); 3 4 if (!experiment || !isExperimentActive(experiment)) { 5 return null; 6 } 7 8 // Check if user is in experiment traffic 9 const inExperiment = isInPercentage(userId, experiment.traffic, experimentKey); 10 if (!inExperiment) { 11 return null; 12 } 13 14 // Assign variant based on weights 15 const totalWeight = experiment.variants.reduce((sum, v) => sum + v.weight, 0); 16 const hash = consistentHash(`${userId}:${experimentKey}:variant`); 17 const bucket = hash % totalWeight; 18 19 let cumulative = 0; 20 for (const variant of experiment.variants) { 21 cumulative += variant.weight; 22 if (bucket < cumulative) { 23 return variant.key; 24 } 25 } 26 27 return experiment.variants[0].key; 28}

Tracking Experiment Results#

1// Track conversions by variant 2analytics.track('checkout_completed', { 3 experiment: 'checkout-button-color', 4 variant: user.experiments['checkout-button-color'], 5 revenue: order.total 6}); 7 8// Analysis 9async function analyzeExperiment(experimentKey: string) { 10 const variants = await getExperimentData(experimentKey); 11 12 return variants.map(v => ({ 13 variant: v.key, 14 users: v.userCount, 15 conversions: v.conversions, 16 conversionRate: v.conversions / v.userCount, 17 revenue: v.totalRevenue, 18 revenuePerUser: v.totalRevenue / v.userCount, 19 statisticalSignificance: calculateSignificance(v, variants[0]) 20 })); 21}

Best Practices#

Naming Conventions#

1// ✅ Good flag names 2'checkout-redesign-2024' // Descriptive, dated 3'premium-feature-early-access' // Purpose clear 4'experiment-button-color-cta' // Type indicated 5'ops-maintenance-mode' // Category prefix 6 7// ❌ Bad flag names 8'test-1' // Not descriptive 9'new-feature' // Too vague 10'johns-test' // Personal 11'temp' // Will never be removed

Flag Lifecycle#

1const flagLifecycle = { 2 states: ['planning', 'development', 'testing', 'rollout', 'complete', 'archived'], 3 4 policies: { 5 maxAge: 90, // Days before requiring review 6 reviewRequired: ['complete'], // States requiring review 7 autoArchive: ['complete'], // Auto-archive after 30 days 8 } 9}; 10 11// Regular cleanup job 12async function cleanupStaleFlags() { 13 const staleFlags = await findStaleFlags(90); // Older than 90 days 14 15 for (const flag of staleFlags) { 16 if (flag.state === 'complete' && flag.rolloutPercentage === 100) { 17 await notifyOwner(flag, 'Consider removing this flag from code'); 18 } 19 } 20}

Technical Debt Prevention#

Design a flag cleanup process: Problems to solve: - Flags remain in code after rollout complete - Code becomes cluttered with dead branches - Testing burden increases with more flags Implement: - Flag expiration dates - Automated PR creation for flag removal - Flag usage tracking - Ownership and review policies

Conclusion#

Feature flags transform deployment from a scary, all-or-nothing event into a controlled, reversible process. With progressive rollouts, you catch problems early, limit blast radius, and ship with confidence.

Start simple: one flag for your next risky feature. As you gain confidence, expand to A/B testing, percentage rollouts, and automated guardrails. The investment pays dividends every time you need to roll back instantly or test a change safely.

AI helps implement these systems correctly, from consistent hashing for stable assignments to automated rollback logic. The result is faster shipping with less risk—exactly what modern development demands.

Share this article

Help spread the word about Bootspring