# 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<string, unknown>;
}

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<string, FeatureFlag> = new Map();
  private evaluator = new FlagEvaluator();
  private context: EvaluationContext = {};
  private overrides: Map<string, unknown> = new Map();

  async initialize(): Promise<void> {
    await this.fetchFlags();

    // Poll for updates
    setInterval(() => this.fetchFlags(), 60000);
  }

  private async fetchFlags(): Promise<void> {
    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<T>(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<string, unknown> {
    const result: Record<string, unknown> = {};

    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: <T>(key: string, defaultValue: T) => T;
  isReady: boolean;
}

const FeatureFlagContext = createContext<FeatureFlagContextValue | null>(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 (
    <FeatureFlagContext.Provider value={value}>
      {children}
    </FeatureFlagContext.Provider>
  );
}

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 (
    <div>
      <h1>Dashboard</h1>

      <Feature flag="new-analytics-dashboard">
        <NewAnalyticsDashboard />
      </Feature>

      <Feature
        flag="ai-insights"
        fallback={<p>AI Insights coming soon!</p>}
      >
        <AIInsightsPanel />
      </Feature>
    </div>
  );
}
```

---

## 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<string, FeatureFlag> = new Map();
let lastFetch = 0;
const CACHE_TTL = 60000; // 1 minute

async function getFlags(): Promise<Map<string, FeatureFlag>> {
  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<boolean> {
  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<EvaluationContext> {
  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 (
    <div>
      <h1>Settings</h1>
      {showBetaFeatures && <BetaSettings />}
    </div>
  );
}
```

---

## 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<void> {
  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<boolean> {
  // 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<void> {
  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<void> {
  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<FeatureFlag[]>([]);
  const [selectedFlag, setSelectedFlag] = useState<FeatureFlag | null>(null);

  useEffect(() => {
    fetch('/api/admin/features')
      .then(res => res.json())
      .then(setFlags);
  }, []);

  const updateFlag = async (key: string, updates: Partial<FeatureFlag>) => {
    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 (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-6">Feature Flags</h1>

      <div className="bg-white rounded-lg shadow">
        <table className="w-full">
          <thead className="bg-gray-50">
            <tr>
              <th className="px-4 py-3 text-left">Flag</th>
              <th className="px-4 py-3 text-left">Status</th>
              <th className="px-4 py-3 text-left">Rollout</th>
              <th className="px-4 py-3 text-left">Actions</th>
            </tr>
          </thead>
          <tbody>
            {flags.map(flag => (
              <tr key={flag.key} className="border-t">
                <td className="px-4 py-3">
                  <div>
                    <div className="font-medium">{flag.name}</div>
                    <div className="text-sm text-gray-500">{flag.key}</div>
                  </div>
                </td>
                <td className="px-4 py-3">
                  <ToggleSwitch
                    enabled={flag.enabled}
                    onChange={(enabled) => updateFlag(flag.key, { enabled })}
                  />
                </td>
                <td className="px-4 py-3">
                  <RolloutSlider
                    value={flag.rolloutPercentage}
                    onChange={(percentage) =>
                      updateFlag(flag.key, { rolloutPercentage: percentage })
                    }
                  />
                </td>
                <td className="px-4 py-3">
                  <button
                    onClick={() => setSelectedFlag(flag)}
                    className="text-blue-600 hover:underline"
                  >
                    Edit Rules
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {selectedFlag && (
        <FlagRulesModal
          flag={selectedFlag}
          onClose={() => setSelectedFlag(null)}
          onSave={(rules) => updateFlag(selectedFlag.key, { rules })}
        />
      )}
    </div>
  );
}
```

---

## 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/)
