# Feature Flags & Progressive Rollout **Meta-Description:** Feature Flags implementieren mit Next.js. Progressive Rollouts, Canary Releases und A/B Testing mit Feature Management. **Keywords:** Feature Flags, Feature Toggle, Progressive Rollout, Canary Release, LaunchDarkly, Unleash, Feature Management --- ## Einführung **Feature Flags** ermöglichen kontinuierliche Deployment ohne Risiko. Features können für bestimmte Nutzer aktiviert, schrittweise ausgerollt oder bei Problemen sofort deaktiviert werden. Dieser Guide zeigt Implementation und Best Practices. --- ## Feature Flags Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ FEATURE FLAGS ARCHITECTURE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Flag Types: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 1. Release Flags: │ │ │ │ - Hide unfinished features │ │ │ │ - Temporary, remove after launch │ │ │ │ │ │ │ │ 2. Experiment Flags: │ │ │ │ - A/B testing │ │ │ │ - Feature comparison │ │ │ │ │ │ │ │ 3. Ops Flags: │ │ │ │ - Kill switches │ │ │ │ - Performance toggles │ │ │ │ │ │ │ │ 4. Permission Flags: │ │ │ │ - Plan-based features │ │ │ │ - User-specific access │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Rollout Strategies: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Percentage Rollout: │ │ │ │ ├── 5% → 25% → 50% → 100% │ │ │ │ └── Gradual exposure to reduce risk │ │ │ │ │ │ │ │ User Targeting: │ │ │ │ ├── Specific user IDs │ │ │ │ ├── User attributes (plan, role, country) │ │ │ │ └── Cohort-based │ │ │ │ │ │ │ │ Environment-based: │ │ │ │ ├── Development: all flags on │ │ │ │ ├── Staging: beta flags on │ │ │ │ └── Production: controlled rollout │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Decision Flow: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Request → Get User Context → Evaluate Rules │ │ │ │ → Check Percentage → Return Flag Value │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Custom Feature Flag System ```typescript // lib/features/types.ts export interface FeatureFlag { key: string; name: string; description: string; type: 'boolean' | 'string' | 'number' | 'json'; defaultValue: unknown; enabled: boolean; rules: TargetingRule[]; rolloutPercentage: number; createdAt: Date; updatedAt: Date; } export interface TargetingRule { id: string; attribute: string; operator: 'equals' | 'contains' | 'greaterThan' | 'lessThan' | 'in' | 'notIn'; value: unknown; enabled: boolean; } export interface EvaluationContext { userId?: string; email?: string; plan?: string; role?: string; country?: string; tenantId?: string; attributes?: Record; } export interface FlagEvaluation { value: unknown; reason: 'default' | 'rule' | 'percentage' | 'override' | 'disabled'; ruleId?: string; } ``` ```typescript // lib/features/evaluator.ts import { createHash } from 'crypto'; import { FeatureFlag, EvaluationContext, FlagEvaluation, TargetingRule } from './types'; export class FlagEvaluator { evaluate( flag: FeatureFlag, context: EvaluationContext ): FlagEvaluation { // Check if flag is globally disabled if (!flag.enabled) { return { value: flag.defaultValue, reason: 'disabled' }; } // Check targeting rules for (const rule of flag.rules) { if (!rule.enabled) continue; if (this.matchesRule(rule, context)) { return { value: true, reason: 'rule', ruleId: rule.id }; } } // Check percentage rollout if (flag.rolloutPercentage > 0 && flag.rolloutPercentage < 100) { const inRollout = this.isInRolloutPercentage( flag.key, context.userId || '', flag.rolloutPercentage ); if (inRollout) { return { value: true, reason: 'percentage' }; } } // 100% rollout if (flag.rolloutPercentage === 100) { return { value: true, reason: 'percentage' }; } // Default value return { value: flag.defaultValue, reason: 'default' }; } private matchesRule(rule: TargetingRule, context: EvaluationContext): boolean { const contextValue = this.getContextValue(rule.attribute, context); switch (rule.operator) { case 'equals': return contextValue === rule.value; case 'contains': return String(contextValue).includes(String(rule.value)); case 'greaterThan': return Number(contextValue) > Number(rule.value); case 'lessThan': return Number(contextValue) < Number(rule.value); case 'in': return Array.isArray(rule.value) && rule.value.includes(contextValue); case 'notIn': return Array.isArray(rule.value) && !rule.value.includes(contextValue); default: return false; } } private getContextValue(attribute: string, context: EvaluationContext): unknown { if (attribute in context) { return context[attribute as keyof EvaluationContext]; } return context.attributes?.[attribute]; } private isInRolloutPercentage( flagKey: string, userId: string, percentage: number ): boolean { // Deterministic hash for consistent assignment const hash = createHash('md5') .update(`${flagKey}:${userId}`) .digest('hex'); const hashValue = parseInt(hash.substring(0, 8), 16) % 100; return hashValue < percentage; } } ``` ```typescript // lib/features/client.ts import { FeatureFlag, EvaluationContext, FlagEvaluation } from './types'; import { FlagEvaluator } from './evaluator'; class FeatureFlagClient { private flags: Map = new Map(); private evaluator = new FlagEvaluator(); private context: EvaluationContext = {}; private overrides: Map = new Map(); async initialize(): Promise { await this.fetchFlags(); // Poll for updates setInterval(() => this.fetchFlags(), 60000); } private async fetchFlags(): Promise { try { const response = await fetch('/api/features'); const flags: FeatureFlag[] = await response.json(); this.flags.clear(); flags.forEach(flag => this.flags.set(flag.key, flag)); } catch (error) { console.error('Failed to fetch feature flags:', error); } } setContext(context: EvaluationContext): void { this.context = context; } // Override for testing/debugging setOverride(key: string, value: unknown): void { this.overrides.set(key, value); } clearOverrides(): void { this.overrides.clear(); } isEnabled(key: string, defaultValue: boolean = false): boolean { // Check overrides first if (this.overrides.has(key)) { return Boolean(this.overrides.get(key)); } const flag = this.flags.get(key); if (!flag) { return defaultValue; } const evaluation = this.evaluator.evaluate(flag, this.context); return Boolean(evaluation.value); } getValue(key: string, defaultValue: T): T { if (this.overrides.has(key)) { return this.overrides.get(key) as T; } const flag = this.flags.get(key); if (!flag) { return defaultValue; } const evaluation = this.evaluator.evaluate(flag, this.context); return (evaluation.value as T) ?? defaultValue; } getEvaluation(key: string): FlagEvaluation | null { const flag = this.flags.get(key); if (!flag) return null; return this.evaluator.evaluate(flag, this.context); } getAllFlags(): Record { const result: Record = {}; this.flags.forEach((flag, key) => { const evaluation = this.evaluator.evaluate(flag, this.context); result[key] = evaluation.value; }); return result; } } // Singleton export const featureFlags = new FeatureFlagClient(); ``` --- ## React Integration ```tsx // contexts/FeatureFlagContext.tsx 'use client'; import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { featureFlags } from '@/lib/features/client'; import { EvaluationContext } from '@/lib/features/types'; interface FeatureFlagContextValue { isEnabled: (key: string, defaultValue?: boolean) => boolean; getValue: (key: string, defaultValue: T) => T; isReady: boolean; } const FeatureFlagContext = createContext(null); export function FeatureFlagProvider({ children, context }: { children: ReactNode; context?: EvaluationContext; }) { const [isReady, setIsReady] = useState(false); useEffect(() => { if (context) { featureFlags.setContext(context); } featureFlags.initialize().then(() => setIsReady(true)); }, [context]); const value: FeatureFlagContextValue = { isEnabled: (key, defaultValue = false) => featureFlags.isEnabled(key, defaultValue), getValue: (key, defaultValue) => featureFlags.getValue(key, defaultValue), isReady }; return ( {children} ); } export function useFeatureFlags() { const context = useContext(FeatureFlagContext); if (!context) { throw new Error('useFeatureFlags must be used within FeatureFlagProvider'); } return context; } // hooks/useFeature.ts export function useFeature(key: string, defaultValue: boolean = false): boolean { const { isEnabled, isReady } = useFeatureFlags(); if (!isReady) return defaultValue; return isEnabled(key, defaultValue); } ``` ```tsx // components/Feature.tsx 'use client'; import { ReactNode } from 'react'; import { useFeature } from '@/hooks/useFeature'; interface FeatureProps { flag: string; children: ReactNode; fallback?: ReactNode; } export function Feature({ flag, children, fallback = null }: FeatureProps) { const isEnabled = useFeature(flag); if (!isEnabled) { return <>{fallback}; } return <>{children}; } // Usage export function Dashboard() { return (

Dashboard

AI Insights coming soon!

} >
); } ``` --- ## Server-Side Evaluation ```typescript // lib/features/server.ts import { cookies, headers } from 'next/headers'; import { db } from '@/lib/db'; import { FlagEvaluator } from './evaluator'; import { FeatureFlag, EvaluationContext } from './types'; const evaluator = new FlagEvaluator(); // Cache flags in memory let flagsCache: Map = new Map(); let lastFetch = 0; const CACHE_TTL = 60000; // 1 minute async function getFlags(): Promise> { if (Date.now() - lastFetch < CACHE_TTL && flagsCache.size > 0) { return flagsCache; } const flags = await db.featureFlag.findMany({ include: { rules: true } }); flagsCache = new Map(flags.map(f => [f.key, f])); lastFetch = Date.now(); return flagsCache; } export async function isFeatureEnabled( key: string, context?: EvaluationContext ): Promise { const flags = await getFlags(); const flag = flags.get(key); if (!flag) return false; // Build context from request if not provided const evalContext = context || await buildContextFromRequest(); const evaluation = evaluator.evaluate(flag, evalContext); return Boolean(evaluation.value); } async function buildContextFromRequest(): Promise { const headersList = await headers(); const cookieStore = await cookies(); return { userId: cookieStore.get('userId')?.value, tenantId: headersList.get('x-tenant-id') || undefined, country: headersList.get('cf-ipcountry') || undefined }; } // Usage in Server Components export default async function SettingsPage() { const showBetaFeatures = await isFeatureEnabled('beta-features'); return (

Settings

{showBetaFeatures && }
); } ``` --- ## Progressive Rollout ```typescript // lib/features/rollout.ts import { db } from '@/lib/db'; interface RolloutPlan { stages: RolloutStage[]; currentStage: number; startedAt: Date; } interface RolloutStage { percentage: number; duration: number; // hours metrics: { errorRateThreshold: number; latencyThreshold: number; }; } export async function progressRollout(flagKey: string): Promise { const flag = await db.featureFlag.findUnique({ where: { key: flagKey }, include: { rolloutPlan: true } }); if (!flag?.rolloutPlan) { throw new Error('No rollout plan found'); } const plan = flag.rolloutPlan as RolloutPlan; const currentStage = plan.stages[plan.currentStage]; const nextStage = plan.stages[plan.currentStage + 1]; if (!nextStage) { console.log(`Rollout complete for ${flagKey}`); return; } // Check metrics before progressing const metricsOk = await checkRolloutMetrics(flagKey, currentStage.metrics); if (!metricsOk) { // Automatic rollback await rollbackFeature(flagKey); throw new Error('Metrics threshold exceeded, rolling back'); } // Progress to next stage await db.featureFlag.update({ where: { key: flagKey }, data: { rolloutPercentage: nextStage.percentage, rolloutPlan: { ...plan, currentStage: plan.currentStage + 1 } } }); console.log(`Rolled out ${flagKey} to ${nextStage.percentage}%`); } async function checkRolloutMetrics( flagKey: string, thresholds: { errorRateThreshold: number; latencyThreshold: number } ): Promise { // Query your metrics system const metrics = await getFeatureMetrics(flagKey); return ( metrics.errorRate < thresholds.errorRateThreshold && metrics.p95Latency < thresholds.latencyThreshold ); } export async function rollbackFeature(flagKey: string): Promise { await db.featureFlag.update({ where: { key: flagKey }, data: { rolloutPercentage: 0, enabled: false } }); // Alert team await sendSlackAlert(`Feature ${flagKey} automatically rolled back due to metrics`); } // Scheduled job for automatic progression export async function processRollouts(): Promise { const activeRollouts = await db.featureFlag.findMany({ where: { rolloutPercentage: { gt: 0, lt: 100 }, enabled: true } }); for (const flag of activeRollouts) { try { await progressRollout(flag.key); } catch (error) { console.error(`Rollout failed for ${flag.key}:`, error); } } } ``` --- ## Admin Dashboard ```tsx // app/admin/features/page.tsx 'use client'; import { useState, useEffect } from 'react'; import { FeatureFlag } from '@/lib/features/types'; export default function FeatureFlagsAdmin() { const [flags, setFlags] = useState([]); const [selectedFlag, setSelectedFlag] = useState(null); useEffect(() => { fetch('/api/admin/features') .then(res => res.json()) .then(setFlags); }, []); const updateFlag = async (key: string, updates: Partial) => { await fetch(`/api/admin/features/${key}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); setFlags(flags.map(f => f.key === key ? { ...f, ...updates } : f )); }; return (

Feature Flags

{flags.map(flag => ( ))}
Flag Status Rollout Actions
{flag.name}
{flag.key}
updateFlag(flag.key, { enabled })} /> updateFlag(flag.key, { rolloutPercentage: percentage }) } />
{selectedFlag && ( setSelectedFlag(null)} onSave={(rules) => updateFlag(selectedFlag.key, { rules })} /> )}
); } ``` --- ## Best Practices | Practice | Description | |----------|-------------| | **Naming** | Use clear, descriptive flag names | | **Lifecycle** | Remove flags after full rollout | | **Testing** | Test both flag states | | **Documentation** | Document flag purpose and owner | | **Monitoring** | Track flag evaluations | | **Defaults** | Safe defaults (usually off) | --- ## Fazit Feature Flags ermöglichen: 1. **Safe Deployments**: Features schrittweise ausrollen 2. **Quick Rollbacks**: Probleme sofort beheben 3. **Experimentation**: A/B Tests und Experimente 4. **Customization**: Feature-based Pricing Feature Flags sind essentiell für moderne Software-Entwicklung. --- ## Bildprompts 1. "Feature flag dashboard, toggles and rollout percentages" 2. "Progressive rollout diagram, stages from 1% to 100%" 3. "Feature targeting rules interface, user segmentation" --- ## Quellen - [Martin Fowler Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) - [LaunchDarkly Documentation](https://docs.launchdarkly.com/) - [Unleash Feature Management](https://www.getunleash.io/) - [Feature Flags Best Practices](https://featureflags.io/)