Feature Flags: Implementation and Management at Scale
Feature flags (feature toggles) enable trunk-based development and controlled rollouts.
Simple In-Memory Implementation
interface FlagConfig {
enabled: boolean;
percentage?: number; // % of users enabled
allowedUsers?: string[]; // specific user IDs
allowedGroups?: string[]; // user groups
}
class FeatureFlagService {
private flags: Map<string, FlagConfig>;
constructor(config: Record<string, FlagConfig>) {
this.flags = new Map(Object.entries(config));
}
isEnabled(flagName: string, context?: { userId?: string; groups?: string[] }): boolean {
const flag = this.flags.get(flagName);
if (!flag || !flag.enabled) return false;
// User-specific override
if (context?.userId && flag.allowedUsers?.includes(context.userId)) return true;
// Group-based
if (context?.groups && flag.allowedGroups?.some(g => context.groups?.includes(g))) return true;
// Percentage rollout (hash-based for consistency)
if (flag.percentage !== undefined && context?.userId) {
const hash = this.hashUserId(context.userId);
return (hash % 100) < flag.percentage;
}
return flag.enabled && !flag.percentage;
}
private hashUserId(userId: string): number {
let hash = 0;
for (const char of userId) {
hash = ((hash << 5) - hash) + char.charCodeAt(0);
hash |= 0;
}
return Math.abs(hash);
}
}
LaunchDarkly Integration
import * as LaunchDarkly from '@launchdarkly/node-server-sdk';
const client = LaunchDarkly.init(process.env.LD_SDK_KEY!);
async function isFeatureEnabled(
flagKey: string,
user: { key: string; email?: string; groups?: string[] }
): Promise<boolean> {
await client.waitForInitialization();
return client.variation(flagKey, {
key: user.key,
email: user.email,
custom: { groups: user.groups },
}, false);
}
// Usage in Express middleware
app.use(async (req, res, next) => {
const userId = req.user?.id ?? 'anonymous';
req.features = {
newCheckout: await isFeatureEnabled('new-checkout-flow', { key: userId }),
mlRecommendations: await isFeatureEnabled('ml-recommendations', { key: userId }),
};
next();
});
A/B Testing
class ABTestingService {
async getVariant(
testName: string,
userId: string
): Promise<'control' | 'treatment'> {
const hash = hashUserId(userId + testName);
const variant = (hash % 2) === 0 ? 'control' : 'treatment';
// Track assignment for analysis
await analytics.track('ab_test_assignment', {
userId,
testName,
variant,
timestamp: new Date(),
});
return variant;
}
}
// In route handler
const variant = await abTest.getVariant('checkout-button-color', userId);
const buttonColor = variant === 'treatment' ? '#00a651' : '#0066cc';
Flag Cleanup
// Technical debt: flags that should be removed
// Add a TODO with expiry date
const NEW_CHECKOUT = 'new-checkout-flow'; // TODO: remove by 2025-03-01
// Automated stale flag detection
// In CI, check flags older than 30 days
#!/bin/bash
# Find old feature flags in code
grep -r "featureFlags.isEnabled|isFeatureEnabled" src/ | grep -v "node_modules" | awk -F"'" '{print $2}' | sort -u
Environment-Based Flags
// Simple env-var based flags (good for infra/config flags)
const FLAGS = {
USE_NEW_DB: process.env.USE_NEW_DB === 'true',
ENABLE_TRACING: process.env.ENABLE_TRACING === 'true',
};
// But prefer a proper flag service for user-facing features
Feature flags require discipline: create them with a plan to remove them.