Feature Flags: Ship Code Safely With Runtime Control
Feature flags let you deploy code without releasing it. You merge to main, deploy to production, and turn the feature on for 1% of users — or just yourself — until you're confident.
This is how Netflix, Facebook, and every serious engineering team ships software.
The Core Pattern
A feature flag is just a conditional that reads from an external config:
// Without feature flags — risky
function processPayment(order: Order) {
return newPaymentProcessor.charge(order); // Goes live for everyone immediately
}
// With feature flags — controlled rollout
async function processPayment(order: Order) {
const useNewProcessor = await flags.isEnabled('new-payment-processor', {
userId: order.userId,
});
if (useNewProcessor) {
return newPaymentProcessor.charge(order);
}
return legacyPaymentProcessor.charge(order);
}
Build vs Buy
Build it yourself (good for simple on/off flags):
// Simple flag store in Redis
class FeatureFlags {
constructor(private redis: Redis) {}
async isEnabled(flag: string, context: { userId?: string }): Promise<boolean> {
// Check user-specific override first
if (context.userId) {
const userOverride = await this.redis.get(`flag:${flag}:user:${context.userId}`);
if (userOverride !== null) return userOverride === '1';
}
// Fall back to global flag value
const global = await this.redis.get(`flag:${flag}`);
return global === '1';
}
async setFlag(flag: string, enabled: boolean): Promise<void> {
await this.redis.set(`flag:${flag}`, enabled ? '1' : '0');
}
}
Use a service (LaunchDarkly, Unleash, Flagsmith) when you need:
- Percentage rollouts (enable for 10% of users)
- User segmentation (beta users only)
- A/B testing with metrics
- Kill switches that non-engineers can toggle
Percentage Rollout
For gradual rollouts, hash the user ID to get a consistent bucket:
function getUserBucket(userId: string): number {
// Consistent hash — same user always gets same bucket
let hash = 0;
for (const char of userId) {
hash = ((hash << 5) - hash) + char.charCodeAt(0);
hash |= 0;
}
return Math.abs(hash) % 100;
}
async function isEnabled(flag: string, userId: string, rolloutPercent: number): Promise<boolean> {
return getUserBucket(userId) < rolloutPercent;
}
// Enable for 5% of users
const enabled = await isEnabled('new-dashboard', user.id, 5);
Testing With Flags
Feature flags can make testing tricky. Always make flags injectable in tests:
// Good: inject flags, easy to test
async function processOrder(order: Order, flags: FeatureFlags) {
const useFastCheckout = await flags.isEnabled('fast-checkout');
// ...
}
// In tests
const mockFlags = { isEnabled: async () => true };
await processOrder(testOrder, mockFlags);
The Ship Fast Workflow
Feature flags are a core part of continuous deployment:
- Merge feature behind a flag (dark launch)
- Enable for internal team
- Enable for 1% → 10% → 50% → 100%
- Remove the flag after full rollout
This pattern — along with CI/CD, preview environments, and automated testing — is baked into the Ship Fast Skill Pack as a ready-to-use Claude Code workflow. Stop setting up infrastructure from scratch on every project.
Top comments (0)