Menu
Zurück zum Blog
2 min read
Technologie

Feature Flags & Progressive Rollout

Feature Flags implementieren mit Next.js. Progressive Rollouts, Canary Releases und A/B Testing mit Feature Management.

Feature FlagsFeature ToggleProgressive RolloutCanary ReleaseLaunchDarklyUnleash
Feature Flags & Progressive Rollout

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

// 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;
}
// 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;
  }
}
// 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

// 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);
}
// 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

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

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

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

PracticeDescription
**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