Feature Flags and Progressive Delivery
Why Feature Flags?
Without flags: Deploy -> All users get new feature immediately
With flags: Deploy -> Control rollout per user/segment
Use cases:
- Canary releases (0% -> 1% -> 10% -> 50% -> 100%)
- A/B testing (measure impact before full rollout)
- Kill switch (disable broken features without redeploy)
- Beta features for specific users/companies
- Dark launches (ship code, enable later)
Simple Implementation
interface FeatureFlag {
enabled: boolean;
percentage?: number;
userIds?: string[];
segments?: string[];
}
class FeatureFlagService {
private flags = new Map<string, FeatureFlag>();
constructor(private redis: RedisClient) {}
async isEnabled(flagKey: string, user: { id: string; segment?: string }): Promise<boolean> {
const flag = await this.getFlag(flagKey);
if (!flag) return false;
if (!flag.enabled) return false;
// Specific user override
if (flag.userIds?.includes(user.id)) return true;
// Segment-based
if (flag.segments && user.segment && flag.segments.includes(user.segment)) return true;
// Percentage rollout (consistent per user)
if (flag.percentage !== undefined) {
const hash = parseInt(md5(`${flagKey}:${user.id}`).substring(0, 8), 16);
return (hash % 100) < flag.percentage;
}
return flag.enabled;
}
private async getFlag(key: string): Promise<FeatureFlag | null> {
const cached = await this.redis.get(`flag:${key}`);
if (cached) return JSON.parse(cached);
const flag = await this.db.findFlag(key);
if (flag) await this.redis.setEx(`flag:${key}`, 30, JSON.stringify(flag));
return flag;
}
}
// Usage in code
const featureFlags = new FeatureFlagService(redis);
app.get('/checkout', async (req, res) => {
const newCheckout = await featureFlags.isEnabled('new-checkout-flow', req.user);
if (newCheckout) {
return newCheckoutController.handle(req, res);
}
return legacyCheckoutController.handle(req, res);
});
OpenFeature Standard
// OpenFeature - vendor-neutral feature flag standard
import { OpenFeature } from '@openfeature/server-sdk';
import { LaunchDarklyProvider } from '@openfeature/launchdarkly-provider';
await OpenFeature.setProvider(new LaunchDarklyProvider(process.env.LD_SDK_KEY!));
const client = OpenFeature.getClient('my-app');
// Evaluation context
const context = { targetingKey: user.id, email: user.email, plan: user.plan };
// Boolean flag
const newDashboard = await client.getBooleanValue('new-dashboard', false, context);
// String flag (multi-variant)
const checkoutVariant = await client.getStringValue('checkout-variant', 'control', context);
// Number flag
const maxItems = await client.getNumberValue('cart-max-items', 10, context);
Canary Release Flow
// Progressive rollout
async function progressiveRollout(flagKey: string): Promise<void> {
const stages = [1, 5, 10, 25, 50, 100];
for (const percentage of stages) {
await flagService.update(flagKey, { percentage });
console.log(`Rolled out ${flagKey} to ${percentage}%`);
// Monitor for 10 minutes
await waitAndMonitor(10 * 60 * 1000, {
errorRateThreshold: 0.01, // 1% error rate
latencyP99Threshold: 500, // 500ms P99
onThresholdExceeded: async () => {
await flagService.update(flagKey, { percentage: 0, enabled: false });
throw new Error(`Rollback triggered at ${percentage}%`);
}
});
}
}
Feature flags decouple deployment from release - ship code whenever, release features safely.