Feature Flags — Complete Implementation Guide
In this tutorial, you will learn about Feature Flags. We cover key concepts, practical examples, and best practices to help you master this topic.
Feature flags are boolean configuration toggles that control feature availability at runtime, enabling progressive delivery, A/B testing, kill switches, and environment-specific functionality without code deployment.
What You'll Learn
By the end of this tutorial, you will know how to implement feature flags in your backend application, manage flag configurations, implement gradual rollouts, and build a feature flag evaluation engine.
Why It Matters
Feature flags decouple deployment from release. You can deploy code to production that is hidden behind a flag, test it with a subset of users, and gradually roll it out. If something breaks, you flip the flag off without a rollback.
Real-World Use
DodaTech uses feature flags across all 200 Microservices. New features are deployed behind flags and gradually rolled out from 0% to 100% of users over days, with automatic rollback if error rates increase.
Feature Flags Learning Path
flowchart LR
A[Config Pipeline] --> B[Feature Flags]
B --> C[Flag Types]
B --> D[Gradual Rollouts]
B --> E[Kill Switches]
B --> F{You Are Here}
style F fill:#f90,color:#fff
Feature Flag Types
Feature flags come in several categories based on lifespan and purpose.
class FeatureFlagTypes {
static list() {
return [
{
type: "Release Toggle",
lifespan: "Short (days to weeks)",
purpose: "Control rollout of new features",
example: "new-checkout-flow"
},
{
type: "Experiment Toggle",
lifespan: "Medium (weeks to months)",
purpose: "A/B testing and multivariate experiments",
example: "recommendation-algorithm-v2"
},
{
type: "Ops Toggle",
lifespan: "Permanent",
purpose: "Kill switches and operational controls",
example: "disable-payment-processing"
},
{
type: "Permission Toggle",
lifespan: "Permanent",
purpose: "Control features by user tier or role",
example: "premium-analytics-export"
}
];
}
}
console.log("Flag types:", FeatureFlagTypes.list().length);
// Flag types: 4
Simple Feature Flag System
A basic feature flag evaluates boolean toggles against user context.
class FeatureFlagSystem {
constructor() {
this.flags = new Map();
}
addFlag(name, config) {
this.flags.set(name, {
enabled: config.enabled || false,
rolloutPercentage: config.rolloutPercentage || 100,
targetingRules: config.targetingRules || [],
dependencies: config.dependencies || []
});
console.log(`Flag registered: ${name}`);
}
isEnabled(name, context = {}) {
const flag = this.flags.get(name);
if (!flag) {
return false;
}
if (!flag.enabled) {
return false;
}
if (flag.targetingRules.length > 0) {
return this.evaluateTargeting(flag.targetingRules, context);
}
if (flag.rolloutPercentage < 100) {
return this.evaluateRollout(name, context.userId, flag.rolloutPercentage);
}
return true;
}
evaluateTargeting(rules, context) {
return rules.every(rule => {
const value = context[rule.field];
if (!value) return false;
switch (rule.operator) {
case "equals": return value === rule.value;
case "in": return rule.values.includes(value);
case "contains": return value.includes(rule.value);
case "gt": return value > rule.value;
case "lt": return value < rule.value;
default: return false;
}
});
}
evaluateRollout(flagName, userId, percentage) {
const hash = this.hashString(`${flagName}:${userId}`);
return (hash % 100) < percentage;
}
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
}
const flags = new FeatureFlagSystem();
flags.addFlag("new-checkout", {
enabled: true,
rolloutPercentage: 50
});
console.log("Flag enabled for user-123:", flags.isEnabled("new-checkout", { userId: "user-123" }));
console.log("Flag enabled for user-456:", flags.isEnabled("new-checkout", { userId: "user-456" }));
// Flag registered: new-checkout
// Flag enabled for user-123: true
// Flag enabled for user-456: false
Gradual Rollout with Auto-Rollback
Production feature rollouts should increase traffic gradually and roll back automatically on errors.
class GradualRollout {
constructor() {
this.steps = [1, 5, 10, 25, 50, 75, 100];
this.errorThreshold = 0.01;
}
async rollout(featureName, config = {}) {
const steps = config.steps || this.steps;
const interval = config.interval || 3600000;
console.log(`Starting gradual rollout: ${featureName}`);
for (const percentage of steps) {
console.log(` Rolling out to ${percentage}% of users`);
await this.setRolloutPercentage(featureName, percentage);
await this.waitAndEvaluate(featureName, interval);
}
console.log(`Rollout complete: ${featureName}`);
}
async setRolloutPercentage(featureName, percentage) {
// Update the feature flag system
console.log(` Setting ${featureName} to ${percentage}%`);
}
async waitAndEvaluate(featureName, duration) {
console.log(` Waiting ${duration / 60000} minutes for evaluation`);
// Check error rates
const errorRate = await this.getErrorRate(featureName);
if (errorRate > this.errorThreshold) {
console.error(` Error rate ${errorRate} exceeds threshold. Rolling back!`);
await this.setRolloutPercentage(featureName, 0);
throw new Error(`Auto-rollback triggered for ${featureName}`);
}
console.log(` Error rate ${errorRate} within acceptable range`);
}
async getErrorRate(featureName) {
return 0.002;
}
}
const rollout = new GradualRollout();
rollout.rollout("new-recommendations")
.then(() => console.log("Full rollout achieved"))
.catch(err => console.error("Rollout failed:", err.message));
// Starting gradual rollout: new-recommendations
// Rolling out to 1% of users
// Waiting 60 minutes for evaluation
// Error rate 0.002 within acceptable range
// Rolling out to 5% of users
Kill Switch Implementation
A kill switch instantly disables a feature when things go wrong.
class KillSwitchService {
constructor(featureFlags) {
this.featureFlags = featureFlags;
this.killSwitches = new Map();
this.errorCounters = new Map();
}
registerKillSwitch(featureName, config = {}) {
this.killSwitches.set(featureName, {
errorThreshold: config.errorThreshold || 50,
timeWindow: config.timeWindow || 300000,
autoReset: config.autoReset || false
});
this.errorCounters.set(featureName, []);
console.log(`Kill switch registered: ${featureName}`);
}
recordError(featureName) {
const now = Date.now();
const switchConfig = this.killSwitches.get(featureName);
if (!switchConfig) return;
const errors = this.errorCounters.get(featureName);
errors.push(now);
const recentErrors = errors.filter(t => now - t < switchConfig.timeWindow);
this.errorCounters.set(featureName, recentErrors);
if (recentErrors.length >= switchConfig.errorThreshold) {
console.error(`Kill switch triggered: ${featureName}`);
console.error(` ${recentErrors.length} errors in last ${switchConfig.timeWindow / 1000}s`);
this.featureFlags.addFlag(featureName, { enabled: false });
}
}
resetKillSwitch(featureName) {
const switchConfig = this.killSwitches.get(featureName);
if (!switchConfig || !switchConfig.autoReset) return;
this.errorCounters.set(featureName, []);
console.log(`Kill switch reset: ${featureName}`);
}
getStatus(featureName) {
const switchConfig = this.killSwitches.get(featureName);
if (!switchConfig) return { active: false };
const errors = this.errorCounters.get(featureName) || [];
return {
active: switchConfig.enabled !== false,
recentErrors: errors.length,
threshold: switchConfig.errorThreshold,
tripped: errors.length >= switchConfig.errorThreshold
};
}
}
const killSwitch = new KillSwitchService(new FeatureFlagSystem());
killSwitch.registerKillSwitch("new-payment-flow", { errorThreshold: 5, timeWindow: 60000 });
for (let i = 0; i < 6; i++) {
killSwitch.recordError("new-payment-flow");
}
const status = killSwitch.getStatus("new-payment-flow");
console.log("Kill switch tripped:", status.tripped);
// Kill switch registered: new-payment-flow
// Kill switch triggered: new-payment-flow
// 6 errors in last 60s
// Kill switch tripped: true
A/B Testing with Feature Flags
Feature flags enable A/B testing by assigning users to variants.
class ABTestingService {
constructor(featureFlags) {
this.featureFlags = featureFlags;
this.experiments = new Map();
}
createExperiment(name, variants, config = {}) {
this.experiments.set(name, {
variants,
distribution: config.distribution || variants.reduce((acc, v) => {
acc[v] = 100 / variants.length;
return acc;
}, {}),
metrics: config.metrics || []
});
this.featureFlags.addFlag(`experiment:${name}`, {
enabled: true,
rolloutPercentage: 100
});
console.log(`Experiment created: ${name}`);
console.log(` Variants: ${variants.join(", ")}`);
}
getVariant(experimentName, userId) {
const experiment = this.experiments.get(experimentName);
if (!experiment) {
return "control";
}
const hash = this.hashCode(`${experimentName}:${userId}`);
const normalized = Math.abs(hash) % 100;
let cumulative = 0;
for (const [variant, percentage] of Object.entries(experiment.distribution)) {
cumulative += percentage;
if (normalized < cumulative) {
return variant;
}
}
return "control";
}
hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
}
return hash;
}
recordMetric(experimentName, variant, metric, value) {
console.log(`[Experiment ${experimentName}]`);
console.log(` Variant: ${variant}`);
console.log(` Metric: ${metric}`);
console.log(` Value: ${value}`);
}
}
const abTesting = new ABTestingService(new FeatureFlagSystem());
abTesting.createExperiment("checkout-redesign", ["control", "variant-a", "variant-b"], {
distribution: {
"control": 33,
"variant-a": 33,
"variant-b": 34
},
metrics: ["conversion_rate", "avg_order_value", "checkout_abandonment"]
});
const variant = abTesting.getVariant("checkout-redesign", "user-789");
abTesting.recordMetric("checkout-redesign", variant, "conversion_rate", 0.042);
// Experiment created: checkout-redesign
// Variants: control, variant-a, variant-b
// [Experiment checkout-redesign]
// Variant: control
// Metric: conversion_rate
// Value: 0.042
Feature Flag Management Platform
A centralized dashboard for managing feature flags across services.
class FeatureFlagDashboard {
constructor() {
this.flags = new Map();
this.auditLog = [];
this.environments = ["development", "staging", "production"];
}
addFlag(flag) {
this.flags.set(flag.name, {
...flag,
environments: flag.environments || this.environments.reduce((acc, env) => {
acc[env] = { enabled: false, rolloutPercentage: 0 };
return acc;
}, {}),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
});
}
updateFlag(name, environment, config) {
const flag = this.flags.get(name);
if (!flag) throw new Error(`Flag not found: ${name}`);
flag.environments[environment] = {
...flag.environments[environment],
...config
};
flag.updatedAt = new Date().toISOString();
this.auditLog.push({
action: "update",
flag: name,
environment,
changes: config,
timestamp: new Date().toISOString()
});
console.log(`Flag updated: ${name} (${environment})`);
}
getAuditLog(flagName) {
return this.auditLog.filter(entry => entry.flag === flagName);
}
generateReport() {
const report = {
totalFlags: this.flags.size,
enabledFlags: 0,
flagsByEnvironment: {},
recentChanges: this.auditLog.slice(-10)
};
this.environments.forEach(env => {
report.flagsByEnvironment[env] = 0;
});
this.flags.forEach(flag => {
this.environments.forEach(env => {
if (flag.environments[env]?.enabled) {
report.flagsByEnvironment[env]++;
}
});
if (Object.values(flag.environments).some(e => e.enabled)) {
report.enabledFlags++;
}
});
return report;
}
}
const dashboard = new FeatureFlagDashboard();
dashboard.addFlag({
name: "new-checkout",
description: "New checkout flow with improved UX",
owner: "checkout-team",
environments: {
development: { enabled: true, rolloutPercentage: 100 },
staging: { enabled: true, rolloutPercentage: 50 },
production: { enabled: false, rolloutPercentage: 0 }
}
});
dashboard.updateFlag("new-checkout", "production", { enabled: true, rolloutPercentage: 5 });
const report = dashboard.generateReport();
console.log("Flags enabled in production:", report.flagsByEnvironment.production);
// Flag updated: new-checkout (production)
// Flags enabled in production: 1
Common Mistakes
Too many permanent flags -- Flags that are always true or false should be removed. Accumulated flags create dead code and complexity.
No flag cleanup Process -- Without a process to remove flags after rollout, the codebase accumulates Technical Debt. Schedule flag removal as part of the release.
Flags as access control -- Feature flags are for feature availability, not user permissions. Use a proper authorization system for access control.
Missing flag evaluation context -- Flags often need user attributes, device type, or geo-location. Build a context object that provides these consistently.
No monitoring on flagged features -- Without monitoring specific to flagged features, you won't detect problems caused by the new code. Add metrics for each flag.
Nested flag dependencies -- When flag A depends on flag B, turning off B while A is on creates inconsistent states. Model dependencies explicitly.
Practice Questions
What is the difference between a release toggle and an experiment toggle? Release toggles control rollout of new features and are removed after release. Experiment toggles support A/B testing and persist for the experiment duration.
How do you ensure consistent flag evaluation across requests for the same user? Use a deterministic hash based on user ID and flag name. This ensures the same user always gets the same variant.
What should you do when a feature flag is no longer needed? Remove the flag code, delete the flag configuration, and clean up any dead code paths. Add this to the definition of done for every feature.
Challenge: Implement a feature flag service with dependency management and gradual rollout.
class AdvancedFeatureFlags {
constructor() {
this.flags = new Map();
}
addFlag(name, config) {
this.flags.set(name, {
enabled: config.enabled || false,
dependencies: config.dependencies || [],
rollout: config.rollout || { percentage: 100 },
rules: config.rules || []
});
}
evaluate(name, context) {
const flag = this.flags.get(name);
if (!flag) return false;
if (!flag.enabled) return false;
for (const dep of flag.dependencies) {
if (!this.evaluate(dep, context)) {
return false;
}
}
if (flag.rules.length > 0) {
return flag.rules.some(rule => this.matchRule(rule, context));
}
const hash = this.hash(`${name}:${context.userId}`);
return (hash % 100) < flag.rollout.percentage;
}
matchRule(rule, context) {
const value = context[rule.field];
return rule.operator === "in" ? rule.values.includes(value) : value === rule.value;
}
hash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
}
return Math.abs(hash);
}
}
FAQ
Mini Project
Build a complete feature flag management system with gradual rollout, targeting rules, dependency tracking, kill switches, and an audit log.
class CompleteFlagManager {
constructor() {
this.flags = new Map();
this.audit = [];
this.metrics = new Map();
}
createFlag(name, config) {
this.flags.set(name, {
name,
description: config.description || "",
owner: config.owner || "unknown",
enabled: false,
rollout: { percentage: 0 },
rules: config.rules || [],
dependencies: config.dependencies || [],
createdAt: Date.now()
});
this.audit.push({ action: "create", name, timestamp: Date.now() });
}
enableFor(name, context) {
const flag = this.flags.get(name);
if (!flag) return false;
if (!flag.enabled) return false;
for (const dep of flag.dependencies) {
const depFlag = this.flags.get(dep);
if (!depFlag || !depFlag.enabled) return false;
}
if (flag.rules.length > 0) {
return flag.rules.some(rule => {
const val = context[rule.attribute];
return rule.values.includes(val);
});
}
const bucket = this.bucket(context.userId || context.sessionId || "anonymous");
return bucket < flag.rollout.percentage;
}
bucket(id) {
let hash = 0;
for (let i = 0; i < (id || "").length; i++) {
hash = ((hash << 5) - hash) + id.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash) % 100;
}
setRollout(name, percentage) {
const flag = this.flags.get(name);
if (!flag) throw new Error(`Flag ${name} not found`);
flag.rollout.percentage = Math.min(100, Math.max(0, percentage));
flag.enabled = percentage > 0;
this.audit.push({ action: "rollout", name, percentage, timestamp: Date.now() });
}
getAuditTrail(name) {
return this.audit.filter(e => e.name === name);
}
}
const mgr = new CompleteFlagManager();
mgr.createFlag("new-search", { description: "Enhanced search with fuzzy matching", owner: "search-team" });
mgr.setRollout("new-search", 25);
const result = mgr.enableFor("new-search", { userId: "user-123" });
console.log("Flag enabled:", result);
console.log("Audit entries:", mgr.getAuditTrail("new-search").length);
// Flag enabled: true
// Audit entries: 2
What's Next
Now that you understand feature flags, learn about configuration pipeline patterns. Then explore configuration security best practices.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro