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 removedFlag 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.