Feature flag implementation, management, and best practices for gradual rollouts. Use when user mentions "feature flag", "feature toggle", "feature gate", "canary release", "percentage rollout", "A/B testing", "LaunchDarkly", "Unleash", "Flagsmith", "gradual rollout", "kill switch", "dark launch", "trunk-based development with flags", or controlling feature visibility in production.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Feature flag implementation, management, and best practices for gradual rollouts. Use when user mentions "feature flag", "feature toggle", "feature gate", "canary release", "percentage rollout", "A/B testing", "LaunchDarkly", "Unleash", "Flagsmith", "gradual rollout", "kill switch", "dark launch", "trunk-based development with flags", or controlling feature visibility in production.
Feature Flags
Flag Types
Type
Purpose
Lifetime
Example
Release
Gate incomplete features
Days-weeks
new-checkout-flow
Experiment
A/B test variations
Weeks-months
pricing-page-variant
Ops / Kill switch
Runtime control, emergency disable
Permanent-ish
disable-search-indexing
Permission
Entitlement gating
Permanent
premium-analytics
Implementation Patterns
Boolean Flag
if (featureFlags.isEnabled("new-dashboard")) {
return<NewDashboard />;
}
return<LegacyDashboard />;
classFeatureFlagService {
private cache = newMap<string, { flag: FeatureFlag; expiresAt: number }>();
asyncisEnabled(name: string, ctx?: FlagContext): Promise<boolean> {
const flag = awaitthis.getFlag(name);
if (!flag?.enabled) returnfalse;
if (ctx?.userId && flag.allowed_users.includes(ctx.userId)) returntrue;
if (ctx?.segment && flag.allowed_segments.includes(ctx.segment)) returntrue;
if (flag.rollout_percentage > 0 && flag.rollout_percentage < 100 && ctx?.userId) {
return (this.hash(ctx.userId, name) % 100) < flag.rollout_percentage;
}
return flag.rollout_percentage === 100;
}
privatehash(userId: string, flagName: string): number {
let h = 0;
const s = `${userId}:${flagName}`;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
returnMath.abs(h);
}
privateasyncgetFlag(name: string): Promise<FeatureFlag | null> {
const cached = this.cache.get(name);
if (cached && cached.expiresAt > Date.now()) return cached.flag;
const flag = await db.query("SELECT * FROM feature_flags WHERE name = $1", [name]);
if (flag) this.cache.set(name, { flag, expiresAt: Date.now() + 30_000 });
return flag;
}
}
Lifecycle Management
Creation
Name flags with ownership: <team>.<feature> (e.g. payments.new-checkout). Record type, owner, ticket, and expected removal date at creation.
Rollout Schedule
Day 1: Internal only (allowlist) Day 5: 10% (monitor metrics)
Day 3: 1% (smoke test) Day 8: 50% (validate at scale)
Day 10: 100% (full rollout) Day 14: Remove flag + dead code
Cleanup
# Find stale flags — search for references, compare against flag service
grep -r "isEnabled\|getVariant\|checkGate\|isOn" --include="*.ts" | \
grep -oP '"[a-z0-9-]+"' | sort -u > used_flags.txt
Warn on overdue flags during evaluation. Log owner and ticket for flags past expected removal date.
Testing with Flags
Unit Tests -- Override Flag Values
describe("Dashboard", () => {
afterEach(() => featureFlags.clearOverrides());
it("renders new dashboard when flag is on", () => {
featureFlags.override("new-dashboard", true);
expect(render(<Dashboard />).getByTestId("new-dashboard")).toBeInTheDocument();
});
it("renders legacy dashboard when flag is off", () => {
featureFlags.override("new-dashboard", false);
expect(render(<Dashboard />).getByTestId("legacy-dashboard")).toBeInTheDocument();
});
});
Integration Tests -- Test All Variants
for (const variant of ["single-page", "multi-step", "control"]) {
it(`completes purchase with ${variant}`, async () => {
featureFlags.override("checkout-flow", variant);
awaitaddItemToCart(testProduct);
awaitsubmitPayment(testCard);
expect(awaitgetOrderStatus()).toBe("confirmed");
});
}
Avoid Combinatorial Explosion
// BAD: 2^n tests for n flags (5 flags = 32, 10 flags = 1024)// GOOD: Test flags independently, plus critical interactions onlyconst criticalCombos = [
{ "new-checkout": true, "new-payments": true },
{ "new-checkout": true, "new-payments": false },
];
for (const combo of criticalCombos) {
it(`works with ${JSON.stringify(combo)}`, async () => {
Object.entries(combo).forEach(([f, v]) => featureFlags.override(f, v));
});
}
// BAD: Release flag left for months — becomes invisible tech debtif (featureFlags.isEnabled("new-header")) { /* added Jan 2024, never removed */ }
// FIX: Set expiry at creation, enforce with CI lint rules
Flag Dependencies
// BAD: Flags that depend on other flags — undefined states when only one is onif (featureFlags.isEnabled("new-checkout") && featureFlags.isEnabled("new-payments")) {}
// FIX: Single flag controlling related changes togetherif (featureFlags.isEnabled("checkout-v2")) {}
Nested Flags
// BAD: 2^3 = 8 hidden statesif (isEnabled("a")) { if (isEnabled("b")) { if (isEnabled("c")) {} } }
// FIX: Flatten to explicit variantsconst variant = featureFlags.getVariant("feature-bundle"); // "a" | "ab" | "abc" | "control"
Flags as Configuration
// BAD: Operational params stored as flagsconst maxRetries = featureFlags.getVariant("max-retries");
// FIX: Use remote config or env vars for operational parametersconst maxRetries = config.get("maxRetries", 3);
Operational Concerns
Performance and Caching
// Cache flags in memory with background refresh to avoid per-request latencyclassCachedFlagClient {
private cache = newMap<string, { value: any; expiresAt: number }>();
constructor(privateclient: FlagClient, private ttl = 30_000) {
setInterval(() =>this.refreshAll(), ttl); // background refresh
}
asyncisEnabled(flag: string, ctx?: FlagContext): Promise<boolean> {
const c = this.cache.get(flag);
if (c && c.expiresAt > Date.now()) return c.value;
returnthis.evaluate(flag, ctx);
}
}
Safe Defaults
// System must work when flag service is downconst show = await featureFlags.isEnabled("new-feature", context).catch((err) => {
logger.error("Flag evaluation failed", { flag: "new-feature", err });
returnfalse; // safe default: feature off
});