2 min read
TechnologieFeature 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
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
| 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:
- Safe Deployments: Features schrittweise ausrollen
- Quick Rollbacks: Probleme sofort beheben
- Experimentation: A/B Tests und Experimente
- Customization: Feature-based Pricing
Feature Flags sind essentiell für moderne Software-Entwicklung.
Bildprompts
- "Feature flag dashboard, toggles and rollout percentages"
- "Progressive rollout diagram, stages from 1% to 100%"
- "Feature targeting rules interface, user segmentation"