Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Gradual rollout strategies, kill switches, A/B testing integration, flag lifecycle management, and technical debt prevention.
.claude/skills/feature-flag-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-19 | ✗→✓ | ▲ Improved | — | — |
| case-20 | ✗→✓ | ▲ Improved | — | — |
Feature flag strategies for safe deployments, experimentation, and operational control.
typescriptinterface FeatureFlag { key: string type: 'boolean' | 'percentage' | 'variant' | 'segment' enabled: boolean description: string owner: string // Team/person responsible createdAt: Date expiresAt?: Date // Auto-cleanup reminder tags: string[] // 'release', 'experiment', 'ops', 'permission' } // Flag categories by lifecycle enum FlagCategory { RELEASE = 'release', // Temporary: ship behind flag, remove after rollout EXPERIMENT = 'experiment', // Temporary: A/B test, remove after analysis OPS = 'ops', // Long-lived: kill switches, circuit breakers PERMISSION = 'permission', // Long-lived: premium features, entitlements } // Flag configuration 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'), // 3 month max 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'], }, }
typescriptclass FeatureFlagService { constructor(private store: FlagStore) {} // Percentage-based rollout using consistent hashing 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': { // Consistent: same user always gets same result for same flag const hash = this.consistentHash(flagKey, userId) return hash < (flag.percentage ?? 0) } case 'segment': return this.isInSegment(userId, flag.segments ?? []) case 'variant': return true // Use getVariant() instead } } // Deterministic hash: user always gets same bucket for same flag private consistentHash(flagKey: string, userId: string): number { const input = `${flagKey}:${userId}` let hash = 0 for (let i = 0; i < input.length; i++) { const char = input.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash // Convert to 32-bit integer } return Math.abs(hash) % 100 // 0-99 } // Multi-variant for A/B/C testing getVariant(flagKey: string, userId: string): string { const flag = this.store.getFlag(flagKey) if (!flag?.enabled || !flag.variants) return 'control' const hash = this.consistentHash(flagKey, userId) let cumulative = 0 for (const variant of flag.variants) { cumulative += variant.weight if (hash < cumulative) return variant.name } return 'control' } // Segment-based targeting private isInSegment(userId: string, segments: Segment[]): boolean { const user = this.store.getUser(userId) return segments.some(segment => { switch (segment.type) { case 'user_list': return segment.userIds.includes(userId) case 'attribute': return user?.[segment.attribute] === segment.value case 'percentage': return this.consistentHash(segment.id, userId) < segment.value } }) } }
typescript// Operational flags: immediately disable features under load class KillSwitch { private cache = new Map<string, { value: boolean; expiry: number }>() private pollInterval: NodeJS.Timeout constructor(private store: FlagStore, pollMs: number = 5000) { // Poll every 5s for kill switch changes (fast response) 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 } // Fallback: if cache miss, assume NOT killed (fail open) return false } private async refresh(): Promise<void> { try { const switches = await this.store.getKillSwitches() for (const [key, value] of Object.entries(switches)) { this.cache.set(key, { value, expiry: Date.now() + 10_000 }) } } catch { // On error, extend existing cache TTL (don't flip switches on failure) } } destroy(): void { clearInterval(this.pollInterval) } } // Usage in request handler async function handleCheckout(req: Request, res: Response) { if (killSwitch.isKilled('checkout-disabled')) { return res.status(503).json({ error: 'Checkout temporarily unavailable', retryAfter: 300, }) } // ... normal checkout logic }
typescript// Track experiment exposure and outcomes 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.track({ experimentKey: flagKey, variant: featureFlags.getVariant(flagKey, userId), userId, timestamp: new Date(), eventType: 'conversion', metadata: { value }, }) } } // Usage: checkout experiment function CheckoutPage({ user }: { user: User }) { const variant = featureFlags.getVariant('checkout-redesign', user.id) experimentTracker.trackExposure('checkout-redesign', variant, user.id) if (variant === 'new') { return <NewCheckout onComplete={() => { experimentTracker.trackConversion('checkout-redesign', user.id) }} /> } return <OldCheckout onComplete={() => { experimentTracker.trackConversion('checkout-redesign', user.id) }} /> }
typescript// Prevent flag debt: enforce expiration dates 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) { // Expired flags: should have been removed if (flag.expiresAt && flag.expiresAt < now) { report.expired.push(flag.key) } // No owner: nobody responsible for cleanup if (!flag.owner) { report.noOwner.push(flag.key) } // Stale: 100% rollout for > 2 weeks (just remove the flag) if (flag.type === 'percentage' && flag.percentage === 100) { const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000) if (flag.lastModified && flag.lastModified < twoWeeksAgo) { report.stale.push(flag.key) } } // Permanent flags without ops/permission tag if (!flag.expiresAt && !flag.tags.includes('ops') && !flag.tags.includes('permission')) { report.permanent.push(flag.key) } } return report } // Run weekly: flag cleanup report // Slack alert for expired/stale flags
if flagA && flagB && !flagC becomes unmaintainable| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.