| name | feature-flags |
| description | Feature flag patterns for controlled rollouts, A/B testing, and kill switches. Use when implementing feature toggles, gradual rollouts, canary releases, percentage-based features, user targeting, or emergency kill switches. |
Feature Flags
Overview
Feature flags (also called feature toggles) enable runtime control over feature availability without code deployments. They support gradual rollouts, A/B testing, user targeting, and emergency kill switches. This skill covers implementation patterns, best practices, and integration with popular tools.
Key Concepts
Flag Types
Boolean Flags - Simple on/off toggles:
interface BooleanFlag {
key: string;
enabled: boolean;
description: string;
}
if (featureFlags.isEnabled('new-checkout-flow')) {
return <NewCheckoutFlow />;
}
return <LegacyCheckout />;
Percentage Rollout Flags - Gradual exposure:
interface PercentageFlag {
key: string;
percentage: number;
salt: string;
}
function isEnabledForUser(flag: PercentageFlag, userId: string): boolean {
const hash = createHash("md5").update(`${flag.salt}:${userId}`).digest("hex");
const bucket = parseInt(hash.substring(0, 8), 16) % 100;
return bucket < flag.percentage;
}
User-Targeted Flags - Specific user segments:
interface TargetedFlag {
key: string;
defaultValue: boolean;
rules: TargetingRule[];
}
interface TargetingRule {
attribute: string;
operator: "in" | "notIn" | "equals" | "contains" | "startsWith" | "matches";
values: string[];
value: boolean;
}
const flag: TargetedFlag = {
key: "advanced-analytics",
defaultValue: false,
rules: [
{
attribute: "email",
operator: "in",
values: ["beta@example.com"],
value: true,
},
{
attribute: "plan",
operator: "in",
values: ["premium", "enterprise"],
value: true,
},
{ attribute: "country", : , : [, ], : },
],
};
Multivariate Flags - Multiple variants for A/B testing:
interface MultivariateFlag<T> {
key: string;
variants: Variant<T>[];
defaultVariant: string;
}
interface Variant<T> {
name: string;
value: T;
weight: number;
}
const buttonColorFlag: MultivariateFlag<string> = {
key: "checkout-button-color",
defaultVariant: "control",
variants: [
{ name: "control", value: "#007bff", weight: 34 },
{ name: "green", value: "#28a745", weight: 33 },
{ name: "orange", value: "#fd7e14", weight: 33 },
],
};
Custom Implementation
import { Redis } from "ioredis";
import { createHash } from "crypto";
interface FeatureFlag {
key: string;
type: "boolean" | "percentage" | "targeted" | "multivariate";
enabled: boolean;
percentage?: number;
rules?: TargetingRule[];
variants?: Variant<unknown>[];
salt: string;
description: string;
createdAt: Date;
updatedAt: Date;
}
interface EvaluationContext {
userId?: string;
email?: string;
country?: string;
plan?: string;
[key: string]: string | number | boolean | undefined;
}
class FeatureFlagService {
private redis: Redis;
: <, { : ; : }> =
();
cacheTTL = ;
() {
. = redis;
}
(: ): < | > {
cached = ..(key);
(cached && cached. > .()) {
cached.;
}
data = ..();
(!data) ;
: = .(data);
..(key, { flag, : .() + . });
flag;
}
(
: ,
: = {},
): <> {
flag = .(key);
(!flag) ;
(!flag.) ;
(flag.) {
:
;
:
.(flag, context. || );
:
.(flag, context);
:
;
}
}
evaluateVariant<T>(
: ,
: = {},
): <T | > {
flag = .(key);
(!flag || !flag. || !flag.) ;
userId = context. || ;
hash = ()
.()
.();
bucket = (hash.(, ), ) % ;
cumulative = ;
( variant flag.) {
cumulative += variant.;
(bucket < cumulative) {
variant. T;
}
}
(flag.[]?. T) ?? ;
}
(: , : ): {
hash = ()
.()
.();
bucket = (hash.(, ), ) % ;
bucket < (flag. ?? );
}
(
: ,
: ,
): {
(!flag. || flag.. === ) ;
( rule flag.) {
attributeValue = (context[rule.] ?? );
matches = ;
(rule.) {
:
matches = rule..(attributeValue);
;
:
matches = !rule..(attributeValue);
;
:
matches = attributeValue === rule.[];
;
:
matches = rule..( attributeValue.(v));
;
:
matches = rule..( attributeValue.(v));
;
:
matches = rule..( (v).(attributeValue));
;
}
(matches) rule.;
}
;
}
(: ): <> {
..(, .(flag));
..(flag.);
..(, .({ : flag. }));
}
(: ): <> {
..();
..(key);
..(
,
.({ key, : }),
);
}
}
Gradual Rollouts and Canary Releases
interface RolloutStrategy {
type: "linear" | "exponential" | "manual";
startPercentage: number;
targetPercentage: number;
incrementPercentage: number;
intervalMinutes: number;
currentPercentage: number;
startedAt: Date;
pausedAt?: Date;
}
class RolloutManager {
private flags: FeatureFlagService;
async startRollout(
flagKey: string,
strategy: RolloutStrategy,
): Promise<void> {
const flag = await this.flags.getFlag(flagKey);
if (!flag) throw new Error("Flag not found");
flag.percentage = strategy.startPercentage;
flag.rolloutStrategy = strategy;
await this.flags.setFlag(flag);
(strategy. !== ) {
.(flagKey, strategy);
}
}
(
: ,
: ,
): <> {
= () => {
flag = ..(flagKey);
(!flag || flag.?.) ;
current = flag. ?? ;
: ;
(strategy. === ) {
newPercentage = .(
current + strategy.,
strategy.,
);
} {
newPercentage = .(current * , strategy.);
}
flag. = newPercentage;
..(flag);
(newPercentage < strategy.) {
(incrementJob, strategy. * * );
}
};
(incrementJob, strategy. * * );
}
(: ): <> {
flag = ..(flagKey);
(flag?.) {
flag.. = ();
..(flag);
}
}
(: ): <> {
flag = ..(flagKey);
(flag) {
flag. = ;
flag. = ;
..(flag);
}
}
}
A/B Testing Integration
interface Experiment {
id: string;
flagKey: string;
name: string;
hypothesis: string;
metrics: string[];
variants: ExperimentVariant[];
status: "draft" | "running" | "paused" | "completed";
startDate?: Date;
endDate?: Date;
sampleSize: number;
confidenceLevel: number;
}
interface ExperimentVariant {
name: string;
weight: number;
conversions: number;
impressions: number;
}
class ABTestingService {
async trackExposure(
experimentId: string,
variantName: string,
userId: string,
): Promise<void> {
await ..({
: ,
userId,
: {
experimentId,
: variantName,
: (),
},
});
..(
,
,
,
);
}
(
: ,
: ,
: ,
): <> {
variant = ..(
,
userId,
);
(!variant) ;
..({
: ,
userId,
: {
experimentId,
variant,
metric,
: (),
},
});
..(
,
,
,
);
}
(: ): <> {
experiment = .(experimentId);
results = .(
experiment..( (variant) => {
data = ..(
,
);
{
: variant.,
: (data. || ),
: (data. || ),
:
(data. || ) /
(data. || ),
};
}),
);
control = results.( r. === );
treatments = results.( r. !== );
{
experimentId,
results,
: treatments.(
.(
control!,
t,
experiment.,
),
),
};
}
(
: ,
: ,
: ,
): {
p1 = control.;
p2 = treatment.;
n1 = control.;
n2 = treatment.;
pooledP = (p1 * n1 + p2 * n2) / (n1 + n2);
se = .(pooledP * ( - pooledP) * ( / n1 + / n2));
z = (p2 - p1) / se;
zThreshold = confidenceLevel === ? : ;
.(z) > zThreshold;
}
}
Kill Switches
interface KillSwitch {
key: string;
description: string;
affectedServices: string[];
activatedAt?: Date;
activatedBy?: string;
reason?: string;
autoRecoveryMinutes?: number;
}
class KillSwitchService {
private redis: Redis;
private alerting: AlertingService;
async activate(
key: string,
reason: string,
activatedBy: string,
): Promise<void> {
const killSwitch = await this.getKillSwitch(key);
if (!killSwitch) throw new Error("Kill switch not found");
killSwitch.activatedAt = new Date();
killSwitch.activatedBy = activatedBy;
killSwitch.reason = reason;
await this.redis.set(, .(killSwitch));
..(
,
.(killSwitch),
);
..({
: ,
: ,
});
(killSwitch.) {
(
.(key, ),
killSwitch. * * ,
);
}
}
(: , : ): <> {
killSwitch = .(key);
(!killSwitch) ;
killSwitch. = ;
killSwitch. = ;
killSwitch. = ;
..(, .(killSwitch));
..(
,
.({ key, reason }),
);
..({
: ,
: ,
});
}
(: ): <> {
data = ..();
(!data) ;
: = .(data);
!!killSwitch.;
}
}
(): <> {
( killSwitches.()) {
(
,
);
}
paymentProcessor.(payment);
}
Flag Lifecycle Management
interface FlagLifecycle {
key: string;
status:
| "planning"
| "development"
| "testing"
| "rollout"
| "stable"
| "deprecated"
| "removed";
owner: string;
team: string;
createdAt: Date;
plannedRemovalDate?: Date;
jiraTicket?: string;
staleAfterDays: number;
}
class FlagLifecycleManager {
async checkStaleFlags(): Promise<StaleFlag[]> {
const flags = await this.getAllFlags();
const now = new Date();
return flags.filter((flag) => {
const lifecycle = flag.lifecycle;
if (!lifecycle) return false;
const ageInDays =
(now.getTime() - lifecycle.createdAt.getTime()) / (1000 * * * );
(lifecycle. === && ageInDays > ) ;
(lifecycle. && lifecycle. < now)
;
(
[, ].(lifecycle.) &&
ageInDays > lifecycle.
)
;
;
});
}
(): <> {
staleFlags = .();
{
: (),
: staleFlags.( ({
: flag.,
: flag..,
: flag..,
: .(flag..),
: .(flag),
})),
: {
: staleFlags.,
: .(staleFlags, f..),
: .(staleFlags, f..),
},
};
}
(: ): {
{ status, plannedRemovalDate } = flag.;
(plannedRemovalDate && plannedRemovalDate < ()) {
;
}
(status === ) {
;
}
(status === || status === ) {
;
}
;
}
}
LaunchDarkly Integration
import * as LaunchDarkly from "launchdarkly-node-server-sdk";
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY!);
interface LDUser {
key: string;
email?: string;
name?: string;
custom?: Record<string, string | number | boolean>;
}
async function evaluateFlag(
flagKey: string,
user: LDUser,
defaultValue: boolean,
): Promise<boolean> {
await ldClient.waitForInitialization();
return ldClient.variation(flagKey, user, defaultValue);
}
async function evaluateFlagWithReason(
flagKey: string,
user: LDUser,
defaultValue: boolean,
) {
await ldClient.waitForInitialization();
detail = ldClient.(flagKey, user, defaultValue);
{
: detail.,
: detail.,
: detail.,
};
}
(): {
ldClient.(eventKey, user, data);
}
(): {
ldClient = ();
[value, setValue] = (defaultValue);
( {
(!ldClient) ;
(ldClient.(flagKey, defaultValue));
= () => (newValue);
ldClient.(, handler);
ldClient.(, handler);
}, [ldClient, flagKey, defaultValue]);
value;
}
Best Practices
-
Flag Naming Conventions
- Use descriptive, consistent names:
feature-checkout-v2, experiment-button-color
- Include type prefix:
release-*, experiment-*, ops-*, kill-*
- Avoid abbreviations and ensure team-wide understanding
-
Flag Hygiene
- Set expiration dates for temporary flags
- Remove flags after features are fully rolled out
- Track flag ownership and associated tickets
- Regular cleanup audits (monthly)
-
Testing
- Test all flag states (on, off, each variant)
- Include flag states in integration tests
- Test rollback scenarios
-
Monitoring
- Track flag evaluation counts and latency
- Alert on unusual patterns (sudden spikes, failures)
- Log flag decisions for debugging
-
Documentation
- Document what each flag controls
- Include rollback instructions
- Link to related PRs and tickets
Examples
React Feature Flag Provider
import React, { createContext, useContext, useEffect, useState } from 'react';
interface FeatureFlagContextType {
isEnabled: (key: string) => boolean;
getVariant: <T>(key: string) => T | null;
loading: boolean;
}
const FeatureFlagContext = createContext<FeatureFlagContextType | null>(null);
export function FeatureFlagProvider({ children, userId }: { children: React.ReactNode; userId: string }) {
const [flags, setFlags] = useState<Record<string, unknown>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadFlags() {
const response = await fetch(`/api/flags?userId=${userId}`);
const data = await response.json();
(data);
();
}
();
ws = ();
ws. = {
update = .(event.);
( ({ ...prev, [update.]: update. }));
};
ws.();
}, [userId]);
: = {
: (flags[key]),
: flags[key] T ?? ,
loading,
};
(
);
}
(): {
context = ();
(!context) ();
context.(key);
}
() {
newCheckout = ();
(newCheckout) {
;
}
;
}