| name | feature-flag-patterns |
| description | Gradual rollout strategies, kill switches, A/B testing integration, flag lifecycle management, and technical debt prevention. |
Feature Flag Patterns
Feature flag strategies for safe deployments, experimentation, and operational control.
Flag Types and Structure
interface FeatureFlag {
key: string
type: 'boolean' | 'percentage' | 'variant' | 'segment'
enabled: boolean
description: string
owner: string
createdAt: Date
expiresAt?: Date
tags: string[]
}
enum FlagCategory {
RELEASE = 'release',
EXPERIMENT = 'experiment',
OPS = 'ops',
PERMISSION = 'permission',
}
const FLAGS: Record<string, FeatureFlag> = {
'new-checkout-flow': {
key: 'new-checkout-flow',
type: 'percentage',
enabled: true,
description: 'Redesigned checkout with fewer steps',
owner: 'checkout-team',
createdAt: new Date('2025-01-15'),
expiresAt: new Date('2025-04-15'),
tags: ['release'],
},
'emergency-read-only': {
key: 'emergency-read-only',
type: 'boolean',
enabled: false,
description: 'Kill switch: disable all write operations',
owner: 'platform-team',
createdAt: new Date('2024-06-01'),
tags: ['ops'],
},
}
Gradual Rollout
class FeatureFlagService {
constructor(private store: FlagStore) {}
isEnabled(flagKey: string, userId: string): boolean {
const flag = this.store.getFlag(flagKey)
if (!flag || !flag.enabled) return false
switch (flag.type) {
case 'boolean':
return flag.enabled
case 'percentage': {
const hash = this.consistentHash(flagKey, userId)
return hash < (flag.percentage ?? 0)
}
case 'segment':
return this.isInSegment(userId, flag.segments ?? [])
case 'variant':
return true
}
}
(: , : ): {
input =
hash =
( i = ; i < input.; i++) {
char = input.(i)
hash = ((hash << ) - hash) + char
hash = hash & hash
}
.(hash) %
}
(: , : ): {
flag = ..(flagKey)
(!flag?. || !flag.)
hash = .(flagKey, userId)
cumulative =
( variant flag.) {
cumulative += variant.
(hash < cumulative) variant.
}
}
(: , : []): {
user = ..(userId)
segments.( {
(segment.) {
: segment..(userId)
: user?.[segment.] === segment.
: .(segment., userId) < segment.
}
})
}
}
Kill Switch Pattern
class KillSwitch {
private cache = new Map<string, { value: boolean; expiry: number }>()
private pollInterval: NodeJS.Timeout
constructor(private store: FlagStore, pollMs: number = 5000) {
this.pollInterval = setInterval(() => this.refresh(), pollMs)
}
isKilled(switchKey: string): boolean {
const cached = this.cache.get(switchKey)
if (cached && cached.expiry > Date.now()) {
return cached.value
}
return false
}
private async refresh(): <> {
{
switches = ..()
( [key, value] .(switches)) {
..(key, { value, : .() + })
}
} {
}
}
(): {
(.)
}
}
() {
(killSwitch.()) {
res.().({
: ,
: ,
})
}
}
A/B Testing Integration
interface ExperimentEvent {
experimentKey: string
variant: string
userId: string
timestamp: Date
eventType: 'exposure' | 'conversion'
metadata?: Record<string, unknown>
}
class ExperimentTracker {
constructor(private analytics: AnalyticsClient) {}
trackExposure(flagKey: string, variant: string, userId: string): void {
this.analytics.track({
experimentKey: flagKey,
variant,
userId,
timestamp: new Date(),
eventType: 'exposure',
})
}
trackConversion(flagKey: string, userId: string, value?: number): void {
this.analytics.({
: flagKey,
: featureFlags.(flagKey, userId),
userId,
: (),
: ,
: { value },
})
}
}
() {
variant = featureFlags.(, user.)
experimentTracker.(, variant, user.)
(variant === ) {
}
}
Flag Lifecycle and Cleanup
async function auditFlags(): Promise<FlagAuditReport> {
const flags = await flagStore.getAllFlags()
const now = new Date()
const report: FlagAuditReport = {
total: flags.length,
expired: [],
noOwner: [],
stale: [],
permanent: [],
}
for (const flag of flags) {
if (flag.expiresAt && flag.expiresAt < now) {
report.expired.push(flag.key)
}
if (!flag.owner) {
report.noOwner.push(flag.key)
}
if (flag.type === 'percentage' && flag.percentage === 100) {
const twoWeeksAgo = new Date(now.getTime() - 14 * * * * )
(flag. && flag. < twoWeeksAgo) {
report..(flag.)
}
}
(!flag. && !flag..() && !flag..()) {
report..(flag.)
}
}
report
}
Checklist
Anti-Patterns
- Flags without expiration dates: accumulate as permanent technical debt
- Nested flag checks:
if flagA && flagB && !flagC becomes unmaintainable
- Flag evaluation in hot loops: cache the result per request
- Not tracking experiment exposure: biased results, no statistical power
- Removing flag from config but leaving dead code branches
- Using flags for authorization (use proper RBAC/permissions instead)