Feature Flags for Safe Deployments
Feature flags (also called feature toggles) are a powerful technique that allows you to deploy code to production without immediately releasing it to users. This separation of deployment from release is fundamental to modern software delivery, enabling safe rollouts, A/B testing, and instant rollbacks.
For legacy modernization, feature flags are essential—they allow you to ship new code behind a toggle, gradually enable it for users, and instantly disable it if problems arise.
Video Summary
The video presentation covers feature flags comprehensively:
- The difference between deployment and release
- Types of feature flags and when to use each
- Implementing feature flags in legacy systems
- Gradual rollout strategies and targeting rules
- Managing flag lifecycle to prevent flag debt
- Integration with monitoring and incident response
- Tools and frameworks for feature flag management
Key Concepts
1. Decouple Deployment from Release
Traditional approach:
- Code merged → Code deployed → Feature live for all users
- Risky: Problems affect everyone immediately
- Stressful: Deployments are high-stakes events
With feature flags:
- Code merged → Code deployed behind flag → Feature gradually enabled
- Safe: Problems affect small percentage initially
- Calm: Deployments are low-risk, frequent events
function checkout(order: Order) {
if (featureFlags.isEnabled('new-checkout-flow')) {
return newCheckoutFlow(order);
}
return legacyCheckoutFlow(order);
}
2. Types of Feature Flags
Release Flags (Short-lived)
- Purpose: Gradually roll out new features
- Lifetime: Days to weeks
- Example: Enabling new UI for percentage of users
if (featureFlags.isEnabled('react-dashboard', userId)) {
return <ReactDashboard />;
}
return <LegacyDashboard />;
Experiment Flags (Short-lived)
- Purpose: A/B testing and experiments
- Lifetime: Duration of experiment
- Example: Testing two checkout flows to see which converts better
const variant = featureFlags.getVariant('checkout-experiment', userId);
if (variant === 'new-flow') {
return newCheckoutFlow(order);
}
return currentCheckoutFlow(order);
Ops Flags (Long-lived)
- Purpose: Operational control (circuit breakers, rate limiting)
- Lifetime: Permanent
- Example: Disable expensive feature during high load
if (featureFlags.isEnabled('enable-recommendations')) {
recommendations = await fetchRecommendations(userId);
}
Permission Flags (Long-lived)
- Purpose: Control access to features by user role/plan
- Lifetime: Permanent
- Example: Premium features only for paid users
if (featureFlags.isEnabledForUser('advanced-analytics', user)) {
return <AdvancedAnalytics />;
}
return <BasicAnalytics />;
3. Targeting and Rollout Strategies
Percentage Rollout
// Enable for 25% of users
if (featureFlags.isEnabledForPercentage('new-feature', userId, 25)) {
return newImplementation();
}
User Targeting
// Enable for specific users (internal testing)
if (featureFlags.isEnabledForUsers('beta-feature', userId, [
'user123',
'user456',
])) {
return betaFeature();
}
Attribute-Based Targeting
// Enable for users in specific regions
if (featureFlags.isEnabledForAttributes('eu-compliance', {
userId,
region: user.region,
isPremium: user.plan === 'premium',
})) {
return gdprCompliantFlow();
}
Ring Deployment
// Progressive rollout by ring
const ring = getUserRing(userId);
if (ring === 'internal' ||
ring === 'alpha' ||
(ring === 'beta' && featureFlags.isEnabled('beta-rollout')) ||
(ring === 'production' && featureFlags.isEnabled('production-rollout'))) {
return newFeature();
}
4. Feature Flag Lifecycle
Creation → Rollout → Cleanup
- Create flag - Default to OFF
- Test internally - Enable for dev team
- Alpha rollout - 5% of users
- Monitor metrics - Error rates, performance, business KPIs
- Expand rollout - 25%, 50%, 75%, 100%
- Cleanup - Remove flag after 100% for 2 weeks
Critical: Clean up old flags to avoid flag debt!
Real-World Applications
Example 1: Legacy Database Migration
class UserRepository {
constructor(
private mysql: MySQLUserRepository,
private postgres: PostgresUserRepository,
private featureFlags: FeatureFlags
) {}
async getUser(id: string): Promise<User> {
// Gradual migration from MySQL to Postgres
const rolloutPercentage = this.featureFlags.getRolloutPercentage(
'postgres-migration'
);
const usePostgres = this.shouldUsePostgres(id, rolloutPercentage);
if (usePostgres) {
metrics.increment('db.postgres.read');
return this.postgres.getUser(id);
}
metrics.increment('db.mysql.read');
return this.mysql.getUser(id);
}
private shouldUsePostgres(id: string, percentage: number): boolean {
// Consistent hashing ensures same user always uses same DB
const hash = this.hashUserId(id);
return (hash % 100) < percentage;
}
private hashUserId(id: string): number {
// Simple hash for consistent user assignment
return id.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
}
}
Example 2: API Version Migration
class APIRouter {
async handleRequest(req: Request): Promise<Response> {
const apiVersion = this.featureFlags.getStringValue(
'api-version',
req.user.id,
'v1'
);
switch (apiVersion) {
case 'v2':
return this.v2Handler.handle(req);
case 'v1':
default:
return this.v1Handler.handle(req);
}
}
}
// Gradual migration:
// Week 1: 5% on v2
// Week 2: 25% on v2
// Week 3: 50% on v2
// Week 4: 100% on v2
// Week 6: Remove v1 code
Example 3: Circuit Breaker for Legacy System
class PaymentService {
async processPayment(payment: Payment): Promise<PaymentResult> {
// Ops flag: disable payments if legacy system is struggling
if (!this.featureFlags.isEnabled('enable-payment-processing')) {
throw new ServiceUnavailableError('Payments temporarily disabled');
}
try {
return await this.legacyPaymentSystem.process(payment);
} catch (error) {
// Auto-disable if error rate too high
await this.circuitBreaker.recordFailure();
throw error;
}
}
}
Example 4: Kill Switch for Expensive Features
class RecommendationEngine {
async getRecommendations(userId: string): Promise<Product[]> {
// Kill switch for expensive ML recommendations during high traffic
if (!this.featureFlags.isEnabled('ml-recommendations')) {
// Fallback to simple rule-based recommendations
return this.ruleBasedRecommendations(userId);
}
return this.mlRecommendations(userId);
}
}
Common Pitfalls
1. Flag Debt (Abandoned Flags)
Problem: Flags created but never cleaned up, creating unmaintainable code.
// Code becomes unreadable with nested flags
if (flags.enabled('flag-a')) {
if (flags.enabled('flag-b')) {
if (flags.enabled('flag-c')) {
// What is this code for?
}
}
}
Solution: Flag lifecycle policy and automated cleanup:
// Add creation date to flags
const FLAG_METADATA = {
'new-checkout': {
created: '2024-01-01',
type: 'release',
owner: 'checkout-team',
cleanupAfter: '2024-02-01',
},
};
// Alert on old flags
function auditFlags() {
const now = new Date();
Object.entries(FLAG_METADATA).forEach(([name, metadata]) => {
if (now > new Date(metadata.cleanupAfter)) {
alerting.warn(`Flag ${name} should be cleaned up`);
}
});
}
2. Testing Complexity
Problem: Need to test all flag combinations.
With 3 flags, there are 2³ = 8 combinations to test!
Solution: Test critical paths and use contract tests:
// Test both paths
describe('Checkout', () => {
it('should work with new flow', () => {
featureFlags.enable('new-checkout');
const result = checkout(order);
expect(result.status).toBe('success');
});
it('should work with legacy flow', () => {
featureFlags.disable('new-checkout');
const result = checkout(order);
expect(result.status).toBe('success');
});
});
3. Performance Overhead
Problem: Checking flags on every request adds latency.
Solution: Cache flag values:
class FeatureFlagCache {
private cache = new Map<string, boolean>();
private cacheExpiry = new Map<string, number>();
async isEnabled(flag: string, userId: string): Promise<boolean> {
const cacheKey = `${flag}:${userId}`;
const expiry = this.cacheExpiry.get(cacheKey);
// Return cached value if fresh
if (expiry && Date.now() < expiry) {
return this.cache.get(cacheKey)!;
}
// Fetch and cache
const value = await this.featureFlagService.isEnabled(flag, userId);
this.cache.set(cacheKey, value);
this.cacheExpiry.set(cacheKey, Date.now() + 60000); // 1 min cache
return value;
}
}
4. Flag State Inconsistency
Problem: User experiences flag toggling mid-session.
Solution: Pin flag state at session start:
class SessionFeatureFlags {
private sessionFlags = new Map<string, boolean>();
async initializeSession(userId: string) {
// Evaluate all flags once at session start
const flags = await this.featureFlagService.getAllFlags(userId);
flags.forEach((value, name) => {
this.sessionFlags.set(name, value);
});
}
isEnabled(flag: string): boolean {
return this.sessionFlags.get(flag) ?? false;
}
}
Implementing Feature Flags
Simple In-Memory Implementation
class SimpleFeatureFlags {
private flags = new Map<string, boolean>();
enable(flag: string) {
this.flags.set(flag, true);
}
disable(flag: string) {
this.flags.set(flag, false);
}
isEnabled(flag: string): boolean {
return this.flags.get(flag) ?? false;
}
}
Percentage Rollout
class PercentageFeatureFlags {
private rollouts = new Map<string, number>();
setRolloutPercentage(flag: string, percentage: number) {
this.rollouts.set(flag, percentage);
}
isEnabled(flag: string, userId: string): boolean {
const percentage = this.rollouts.get(flag) ?? 0;
const hash = this.hashString(userId);
return (hash % 100) < percentage;
}
private hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash);
}
}
Production-Ready Solutions
For production use, consider established tools:
- LaunchDarkly - Full-featured SaaS
- Unleash - Open-source, self-hosted
- Flagsmith - Open-source with SaaS option
- Split.io - Enterprise feature flagging and experimentation
Key Takeaways
- Feature flags decouple deployment from release, enabling safer rollouts
- Different flag types (release, experiment, ops, permission) serve different purposes
- Gradual rollout strategies minimize blast radius of changes
- Clean up short-lived flags promptly to avoid technical debt
- Flags are essential for legacy modernization, enabling reversible changes
- Use production-ready flag management tools for complex requirements
- Monitor flag impact on performance and user experience
Further Reading
- Feature Toggles - Martin Fowler's comprehensive guide
- Feature Flag Best Practices - LaunchDarkly's recommendations
- Testing with Feature Flags - How to test flagged code
- Continuous Delivery - Jez Humble's book covering deployment practices