Feature Flag System
Overview
Implement feature flags to decouple deployment from release, enable gradual rollouts, A/B testing, and provide emergency kill switches.
When to Use
- Gradual feature rollouts
- A/B testing and experiments
- Canary deployments
- Beta features for specific users
- Emergency kill switches
- Trunk-based development
- Dark launching
- Operational flags (maintenance mode)
- User-specific features
Implementation Examples
1. Feature Flag Service (TypeScript)
interface FlagConfig {
key: string;
enabled: boolean;
description: string;
rules?: FlagRule[];
variants?: FlagVariant[];
createdAt: Date;
updatedAt: Date;
}
interface FlagRule {
type: "user" | "percentage" | "attribute" | "datetime";
operator: "in" | "equals" | "contains" | "gt" | "lt" | "between";
attribute?: string;
values: any[];
}
interface FlagVariant {
key: string;
weight: number;
value: any;
}
interface EvaluationContext {
userId?: string;
email?: string;
attributes?: Record<string, any>;
timestamp?: number;
}
class FeatureFlagService {
private flags: Map<string, FlagConfig> = new Map();
constructor() {
this.loadFlags();
}
private loadFlags(): void {
// Load from database or config
this.flags.set("new-dashboard", {
key: "new-dashboard",
enabled: true,
description: "New dashboard UI",
rules: [
{
type: "percentage",
operator: "lt",
values: [25], // 25% rollout
},
],
createdAt: new Date(),
updatedAt: new Date(),
});
this.flags.set("premium-features", {
key: "premium-features",
enabled: true,
description: "Premium features for paid users",
rules: [
{
type: "attribute",
operator: "equals",
attribute: "plan",
values: ["premium", "enterprise"],
},
],
createdAt: new Date(),
updatedAt: new Date(),
});
this.flags.set("beta-feature", {
key: "beta-feature",
enabled: true,
description: "Beta feature",
rules: [
{
type: "user",
operator: "in",
values: ["user1", "user2", "user3"],
},
],
createdAt: new Date(),
updatedAt: new Date(),
});
}
isEnabled(flagKey: string, context: EvaluationContext = {}): boolean {
const flag = this.flags.get(flagKey);
if (!flag) {
console.warn(`Flag not found: ${flagKey}`);
return false;
}
if (!flag.enabled) {
return false;
}
if (!flag.rules || flag.rules.length === 0) {
return true;
}
return this.evaluateRules(flag.rules, context);
}
getVariant(flagKey: string, context: EvaluationContext = {}): any {
const flag = this.flags.get(flagKey);
if (!flag || !this.isEnabled(flagKey, context)) {
return null;
}
if (!flag.variants || flag.variants.length === 0) {
return true;
}
return this.selectVariant(flag.variants, context);
}
private evaluateRules(
rules: FlagRule[],
context: EvaluationContext,
): boolean {
return rules.every((rule) => this.evaluateRule(rule, context));
}
private evaluateRule(rule: FlagRule, context: EvaluationContext): boolean {
switch (rule.type) {
case "user":
return this.evaluateUserRule(rule, context);
case "percentage":
return this.evaluatePercentageRule(rule, context);
case "attribute":
return this.evaluateAttributeRule(rule, context);
case "datetime":
return this.evaluateDateTimeRule(rule, context);
default:
return false;
}
}
private evaluateUserRule(
rule: FlagRule,
context: EvaluationContext,
): boolean {
if (!context.userId) return false;
return rule.values.includes(context.userId);
}
private evaluatePercentageRule(
rule: FlagRule,
context: EvaluationContext,
): boolean {
const hash = this.hashContext(context);
const percentage = (hash % 100) + 1;
return percentage <= rule.values[0];
}
private evaluateAttributeRule(
rule: FlagRule,
context: EvaluationContext,
): boolean {
if (!rule.attribute || !context.attributes) return false;
const value = context.attributes[rule.attribute];
switch (rule.operator) {
case "equals":
return rule.values.includes(value);
case "contains":
return rule.values.some((v) => String(value).includes(v));
case "gt":
return value > rule.values[0];
case "lt":
return value < rule.values[0];
default:
return false;
}
}
private evaluateDateTimeRule(
rule: FlagRule,
context: EvaluationContext,
): boolean {
const now = context.timestamp || Date.now();
if (rule.operator === "between") {
return now >= rule.values[0] && now <= rule.values[1];
}
return false;
}
private selectVariant(
variants: FlagVariant[],
context: EvaluationContext,
): any {
const hash = this.hashContext(context);
const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
const position = hash % totalWeight;
let cumulative = 0;
for (const variant of variants) {
cumulative += variant.weight;
if (position < cumulative) {
return variant.value;
}
}
return variants[0].value;
}
private hashContext(context: EvaluationContext): number {
const str = context.userId || context.email || "anonymous";
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);
}
async createFlag(
config: Omit<FlagConfig, "createdAt" | "updatedAt">,
): Promise<void> {
this.flags.set(config.key, {
...config,
createdAt: new Date(),
updatedAt: new Date(),
});
}
async updateFlag(key: string, updates: Partial<FlagConfig>): Promise<void> {
const flag = this.flags.get(key);
if (!flag) {
throw new Error(`Flag not found: ${key}`);
}
this.flags.set(key, {
...flag,
...updates,
updatedAt: new Date(),
});
}
async deleteFlag(key: string): Promise<void> {
this.flags.delete(key);
}
getAllFlags(): FlagConfig[] {
return Array.from(this.flags.values());
}
}
// Usage
const featureFlags = new FeatureFlagService();
// Simple boolean check
if (featureFlags.isEnabled("new-dashboard", { userId: "user123" })) {
console.log("Show new dashboard");
}
// With user attributes
const hasPremiumFeatures = featureFlags.isEnabled("premium-features", {
userId: "user123",
attributes: { plan: "premium" },
});
// Get variant for A/B testing
const buttonColor = featureFlags.getVariant("button-color-test", {
userId: "user123",
});
2. React Hook for Feature Flags
import { createContext, useContext, ReactNode } from 'react';
interface FeatureFlagContextType {
isEnabled: (key: string) => boolean;
getVariant: (key: string) => any;
}
const FeatureFlagContext = createContext<FeatureFlagContextType | null>(null);
export function FeatureFlagProvider({
children,
userId,
attributes
}: {
children: ReactNode;
userId?: string;
attributes?: Record<string, any>;
}) {
const flagService = new FeatureFlagService();
const context: FeatureFlagContextType = {
isEnabled: (key: string) => {
return flagService.isEnabled(key, { userId, attributes });
},
getVariant: (key: string) => {
return flagService.getVariant(key, { userId, attributes });
}
};
return (
<FeatureFlagContext.Provider value={context}>
{children}
</FeatureFlagContext.Provider>
);
}
export function useFeatureFlag(key: string): boolean {
const context = useContext(FeatureFlagContext);
if (!context) {
throw new Error('useFeatureFlag must be used within FeatureFlagProvider');
}
return context.isEnabled(key);
}
export function useFeatureVariant(key: string): any {
const context = useContext(FeatureFlagContext);
if (!context) {
throw new Error('useFeatureVariant must be used within FeatureFlagProvider');
}
return context.getVariant(key);
}
// Feature component wrapper
export function Feature({
flag,
fallback = null,
children
}: {
flag: string;
fallback?: ReactNode;
children: ReactNode;
}) {
const isEnabled = useFeatureFlag(flag);
return isEnabled ? <>{children}</> : <>{fallback}</>;
}
// Usage in components
function Dashboard() {
const hasNewDashboard = useFeatureFlag('new-dashboard');
const theme = useFeatureVariant('theme-experiment');
return (
<div>
{hasNewDashboard ? <NewDashboard /> : <OldDashboard />}
<Feature flag="premium-features" fallback={<UpgradePrompt />}>
<PremiumContent />
</Feature>
<div style={{ backgroundColor: theme?.backgroundColor }}>
Content with experiment theme
</div>
</div>
);
}
3. Feature Flag with Analytics
interface FlagEvaluationEvent {
flagKey: string;
userId?: string;
result: boolean;
variant?: any;
timestamp: number;
duration: number;
}
class FeatureFlagServiceWithAnalytics extends FeatureFlagService {
private analytics: Analytics;
constructor(analytics: Analytics) {
super();
this.analytics = analytics;
}
isEnabled(flagKey: string, context: EvaluationContext = {}): boolean {
const startTime = Date.now();
const result = super.isEnabled(flagKey, context);
const duration = Date.now() - startTime;
this.trackEvaluation({
flagKey,
userId: context.userId,
result,
timestamp: Date.now(),
duration,
});
return result;
}
getVariant(flagKey: string, context: EvaluationContext = {}): any {
const startTime = Date.now();
const variant = super.getVariant(flagKey, context);
const duration = Date.now() - startTime;
this.trackEvaluation({
flagKey,
userId: context.userId,
result: variant !== null,
variant,
timestamp: Date.now(),
duration,
});
return variant;
}
private trackEvaluation(event: FlagEvaluationEvent): void {
this.analytics.track("feature_flag_evaluated", {
flag_key: event.flagKey,
user_id: event.userId,
result: event.result,
variant: event.variant,
duration_ms: event.duration,
});
}
async getAnalytics(
flagKey: string,
timeRange: { start: Date; end: Date },
): Promise<{
evaluations: number;
uniqueUsers: number;
enabledRate: number;
variantDistribution: Record<string, number>;
}> {
return this.analytics.getFlagAnalytics(flagKey, timeRange);
}
}
4. LaunchDarkly-Style SDK
from typing import Dict, Any, Optional
import hashlib
import json
class FeatureFlagClient:
def __init__(self, sdk_key: str, config: Optional[Dict] = None):
self.sdk_key = sdk_key
self.config = config or {}
self.flags: Dict[str, Dict] = {}
self.initialize()
def initialize(self):
"""Load flags from API or cache."""
# In production, fetch from API
self.flags = {
'new-feature': {
'enabled': True,
'rollout': {
'percentage': 50
}
},
'premium-feature': {
'enabled': True,
'targeting': {
'attribute': 'plan',
'values': ['premium', 'enterprise']
}
}
}
def variation(
self,
flag_key: str,
user: Dict[str, Any],
default: bool = False
) -> bool:
"""Evaluate flag for user."""
flag = self.flags.get(flag_key)
if not flag or not flag.get('enabled'):
return default
# Check targeting rules
if 'targeting' in flag:
if not self._evaluate_targeting(flag['targeting'], user):
return False
# Check percentage rollout
if 'rollout' in flag:
return self._evaluate_rollout(flag['rollout'], user, flag_key)
return True
def variation_detail(
self,
flag_key: str,
user: Dict[str, Any],
default: Any = None
) -> Dict[str, Any]:
"""Get flag variation with details."""
value = self.variation(flag_key, user, default)
return {
'value': value,
'variation_index': 0 if value else 1,
'reason': {
'kind': 'RULE_MATCH' if value else 'OFF'
}
}
def _evaluate_targeting(self, targeting: Dict, user: Dict) -> bool:
"""Evaluate targeting rules."""
attribute = targeting.get('attribute')
values = targeting.get('values', [])
user_value = user.get(attribute)
return user_value in values
def _evaluate_rollout(
self,
rollout: Dict,
user: Dict,
flag_key: str
) -> bool:
"""Evaluate percentage rollout."""
percentage = rollout.get('percentage', 0)
user_id = user.get('id', user.get('email', 'anonymous'))
# Consistent hashing for stable rollout
hash_value = self._hash_user(user_id, flag_key)
bucket = hash_value % 100
return bucket < percentage
def _hash_user(self, user_id: str, flag_key: str) -> int:
"""Hash user ID for consistent bucketing."""
combined = f"{flag_key}:{user_id}"
hash_bytes = hashlib.sha256(combined.encode()).digest()
return int.from_bytes(hash_bytes[:4], byteorder='big')
def track(self, event_name: str, user: Dict, data: Optional[Dict] = None):
"""Track custom event."""
# Send to analytics
pass
def identify(self, user: Dict):
"""Identify user."""
# Update user context
pass
def flush(self):
"""Flush events."""
pass
def close(self):
"""Close client."""
pass
# Usage
client = FeatureFlagClient(sdk_key='your-sdk-key')
user = {
'id': 'user-123',
'email': 'user@example.com',
'plan': 'premium'
}
# Check if feature is enabled
if client.variation('new-feature', user):
print("New feature enabled!")
# Get detailed information
detail = client.variation_detail('premium-feature', user)
print(f"Value: {detail['value']}, Reason: {detail['reason']}")
# Track event
client.track('feature-used', user, {'feature': 'new-feature'})
5. Admin UI for Feature Flags
interface FlagFormData {
key: string;
description: string;
enabled: boolean;
rolloutPercentage?: number;
targetUsers?: string[];
targetAttributes?: Record<string, any>;
}
function FeatureFlagDashboard() {
const [flags, setFlags] = useState<FlagConfig[]>([]);
const flagService = new FeatureFlagService();
useEffect(() => {
loadFlags();
}, []);
const loadFlags = async () => {
const allFlags = flagService.getAllFlags();
setFlags(allFlags);
};
const toggleFlag = async (key: string) => {
const flag = flags.find(f => f.key === key);
if (flag) {
await flagService.updateFlag(key, { enabled: !flag.enabled });
await loadFlags();
}
};
return (
<div className="dashboard">
<h1>Feature Flags</h1>
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
<th>Status</th>
<th>Rollout</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{flags.map(flag => (
<tr key={flag.key}>
<td>{flag.key}</td>
<td>{flag.description}</td>
<td>
<Switch
checked={flag.enabled}
onChange={() => toggleFlag(flag.key)}
/>
</td>
<td>{getRolloutPercentage(flag)}%</td>
<td>
<button onClick={() => editFlag(flag)}>Edit</button>
<button onClick={() => deleteFlag(flag.key)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Best Practices
✅ DO
- Use descriptive flag names
- Document flag purpose and lifecycle
- Implement gradual rollouts
- Track flag evaluations
- Clean up old flags regularly
- Use feature flags for experiments
- Implement kill switches for critical features
- Test both enabled and disabled states
- Use consistent hashing for stable rollouts
- Provide admin UI for non-technical users
❌ DON'T
- Use flags for permanent configuration
- Accumulate technical debt with old flags
- Skip flag cleanup
- Make flags too granular
- Hard-code flag checks everywhere
- Skip analytics and monitoring