Feature Flags mit Claude Code: Sichere Feature-Releases 2026

Deployments ohne Angst: Feature Flags entkoppeln den Deployment-Zeitpunkt vom Release-Zeitpunkt. Claude Code implementiert Trunk-Based Development, Gradual Rollouts und A/B-Tests — von der einfachen Redis-Lösung bis LaunchDarkly-Integration.

Was sind Feature Flags — und warum jedes Team sie braucht

Die klassische Deployment-Angst kennt jeder: Ein neues Feature ist fertig, aber der Release-Termin ist drei Wochen weg. Der Feature-Branch liegt rum, Merge-Konflikte stapeln sich, und wenn es dann live geht, ist der Code schon veraltet. Feature Flags lösen dieses Problem radikal.

Definition: Ein Feature Flag (auch Feature Toggle oder Feature Switch) ist eine Konfigurationsoption, die bestimmten Code-Pfade zur Laufzeit aktiviert oder deaktiviert — ohne Deployment. Du kannst Code deployen, der noch nicht live ist, und ihn dann per Konfigurationsänderung für Nutzer freischalten.

Trunk-Based Development & Release Decoupling

Feature Flags sind das Herzstück von Trunk-Based Development (TBD): Alle Entwickler commiten täglich auf main, kein langlaufender Feature-Branch. Wie ist das möglich? Weil unfertige Features hinter Flags versteckt sind. Der Code ist deployed — aber unsichtbar.

Das Prinzip heißt Release Decoupling: Deploy != Release. Du kannst jederzeit deployen (kontinuierlich), aber den Release-Zeitpunkt separat steuern. Das gibt Business und Engineering gemeinsam die Kontrolle.

Die vier Arten von Feature Flags

🚀

Release Flags

Temporär. Verstecken unfertige Features. Werden entfernt sobald das Feature stabil läuft. Kurzlebig: Tage bis Wochen.

🧪

Experiment Flags

A/B-Tests und Multivariate Tests. Bestimmte Nutzer-Kohorten sehen Variante A oder B. Metrics entscheiden.

🔐

Permission Flags

Langlebig. Schalten Features für bestimmte Nutzergruppen frei: Beta-User, Enterprise-Tier, interne Teams.

🔴

Kill Switch

Notfall-Deaktivierung eines problematischen Features in Sekunden — ohne Deployment, ohne Rollback.

Praxis-Insight

Claude Code und Feature Flag Strategie

Claude Code erkennt im Kontext, welche Art von Flag du brauchst. "Ich will ein Feature für Beta-User freischalten" → Permission Flag mit User-Targeting. "Wir testen zwei Varianten des Checkout-Flows" → Experiment Flag mit randomisierter Zuweisung. Claude Code schlägt die richtige Implementierungsstrategie vor, bevor es Code schreibt.

Selbst implementiert: Feature Flags mit Redis und PostgreSQL

Bevor du ein externes Tool einführst, lohnt sich zu verstehen was intern nötig ist. Claude Code implementiert eine solide Eigenlösung — in TypeScript, typsicher, mit Redis als Backend für schnelle Reads.

Datenbankschema (PostgreSQL)

-- Feature Flags Tabelle in PostgreSQL CREATE TABLE feature_flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), flag_key VARCHAR(100) UNIQUE NOT NULL, enabled BOOLEAN NOT NULL DEFAULT false, rollout_pct SMALLINT DEFAULT 0, -- 0-100 Prozent Gradual Rollout rules JSONB DEFAULT '[]', -- Targeting Rules als JSON description TEXT, expires_at TIMESTAMPTZ, -- Auto-Expiry für Release Flags created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Index für schnelle Lookups CREATE INDEX idx_flags_key ON feature_flags(flag_key); CREATE INDEX idx_flags_expires ON feature_flags(expires_at) WHERE expires_at IS NOT NULL;

TypeScript Service mit Redis-Caching

// feature-flags.service.ts — Type-safe Feature Flag Service import { createClient } from 'redis'; import { Pool } from 'pg'; import { createHash } from 'crypto'; interface FlagContext { userId?: string; email?: string; plan?: 'free' | 'pro' | 'enterprise'; country?: string; betaUser?: boolean; } interface FeatureFlag { flagKey: string; enabled: boolean; rolloutPct: number; rules: TargetingRule[]; expiresAt?: Date; } interface TargetingRule { attribute: keyof FlagContext; operator: 'eq' | 'in' | 'contains'; value: string | string[] | boolean; } export class FeatureFlagService { private redis = createClient({ url: process.env.REDIS_URL }); private pg = new Pool({ connectionString: process.env.DATABASE_URL }); private readonly TTL = 60; // 60s Redis Cache async isEnabled(flagKey: string, ctx: FlagContext = {}): Promise<boolean> { const flag = await this.getFlag(flagKey); if (!flag || !flag.enabled) return false; // Auto-Expiry prüfen if (flag.expiresAt && flag.expiresAt < new Date()) return false; // Targeting Rules auswerten (erste passende Rule gewinnt) for (const rule of flag.rules) { if (this.matchesRule(rule, ctx)) return true; } // Gradual Rollout: deterministisch per userId-Hash if (flag.rolloutPct > 0 && ctx.userId) { const bucket = this.getBucket(ctx.userId, flagKey); return bucket < flag.rolloutPct; } // Kein Targeting: globaler On/Off Zustand return flag.rolloutPct === 100; } private getBucket(userId: string, flagKey: string): number { // Hash userId+flagKey → deterministischer Bucket 0-99 const hash = createHash('sha256') .update(`${userId}-${flagKey}`) .digest('hex'); return parseInt(hash.slice(0, 8), 16) % 100; } private matchesRule(rule: TargetingRule, ctx: FlagContext): boolean { const value = ctx[rule.attribute]; if (value === undefined) return false; switch (rule.operator) { case 'eq': return value === rule.value; case 'in': return (Array.isArray(rule.value) ? rule.value : [rule.value]) .includes(value as string); case 'contains': return String(value).includes(String(rule.value)); default: return false; } } private async getFlag(flagKey: string): Promise<FeatureFlag | null> { const cacheKey = `ff:${flagKey}`; const cached = await this.redis.get(cacheKey); if (cached) return JSON.parse(cached); const { rows } = await this.pg.query( 'SELECT * FROM feature_flags WHERE flag_key = $1', [flagKey] ); if (!rows[0]) return null; const flag: FeatureFlag = { flagKey: rows[0].flag_key, enabled: rows[0].enabled, rolloutPct: rows[0].rollout_pct, rules: rows[0].rules, expiresAt: rows[0].expires_at ? new Date(rows[0].expires_at) : undefined, }; await this.redis.setEx(cacheKey, this.TTL, JSON.stringify(flag)); return flag; } }
Selbst Implementiert

Wann lohnt sich die Eigenlösung?

Wenn du unter 10 aktive Flags hast, dein Team klein ist und du kein externes Billing-Risiko willst. Die Eigenlösung kostet nichts außer Redis + PostgreSQL (die du sowieso hast) und gibt dir volle Kontrolle über Daten-Residency und DSGVO-Compliance.

OpenFeature SDK: Der vendor-neutrale Standard

OpenFeature ist ein CNCF-Projekt (Cloud Native Computing Foundation) und standardisiert die Feature-Flag-API unabhängig vom Backend. Du schreibst deinen Code gegen das OpenFeature SDK — und kannst den Provider (LaunchDarkly, Flagsmith, eigene Impl.) jederzeit wechseln, ohne Applikationscode zu ändern.

Das Provider-Pattern

// openfeature-setup.ts — OpenFeature SDK konfigurieren import { OpenFeature, Client } from '@openfeature/server-sdk'; import { LaunchDarklyProvider } from '@openfeature/launchdarkly-provider'; // Einmalig beim App-Start: Provider registrieren await OpenFeature.setProviderAndWait( new LaunchDarklyProvider(process.env.LD_SDK_KEY!) ); // Client für dein Modul const featureClient: Client = OpenFeature.getClient('checkout'); // Typsicheres Auswerten — Boolean const newCheckout = await featureClient.getBooleanValue( 'new-checkout-flow', // Flag-Key false, // Default-Wert (Fallback) { targetingKey: user.id, plan: user.plan } // Evaluation Context ); // String-Variante für Experiments (A/B/C) const variant = await featureClient.getStringValue( 'checkout-layout', 'control', { targetingKey: user.id } ); // → 'control' | 'variant-a' | 'variant-b'

Context-basierte Auswertung

OpenFeature unterscheidet zwischen Evaluation Context (wer fragt?) und Flag-Konfiguration (für wen ist es an?). Du übergibst Nutzer-Attribute als Context — der Provider entscheidet anhand der Targeting-Konfiguration.

// Globaler Context für alle Requests setzen (z.B. in Middleware) import { OpenFeature } from '@openfeature/server-sdk'; // Express Middleware: User-Context aus JWT einlesen export const featureFlagMiddleware = (req, res, next) => { const { userId, email, plan, betaAccess } = req.user ?? {}; OpenFeature.setTransactionContext( { targetingKey: userId ?? 'anonymous', email, plan, betaUser: betaAccess === true, country: req.headers['cf-ipcountry'] ?? 'unknown', }, next // Async Context Propagation via AsyncLocalStorage ); }; // In beliebigem Service-Layer — kein Context-Parameter nötig! const isNewDashboard = await featureClient.getBooleanValue('new-dashboard', false);
OpenFeature Vorteil: Du kannst in der Entwicklung einen In-Memory-Provider nutzen (Flag-Werte hardcoded im Code), in Staging einen eigenen Redis-Provider, und in Produktion LaunchDarkly — ohne eine Zeile Applikationscode zu ändern. Das macht Lokaltests trivial einfach.

LaunchDarkly Integration: Enterprise-Grade Feature Management

LaunchDarkly ist der Marktführer für Feature Management. Das SDK cached Flags lokal, streamt Updates in Echtzeit via Server-Sent Events, und hat integrierten Fallback wenn der LD-Service nicht erreichbar ist. Claude Code integriert LaunchDarkly in wenigen Minuten.

LaunchDarkly

Was LaunchDarkly besser macht als Eigenlösungen

Echtzeit-Updates ohne Cache-Invalidierung, Flag-Targeting über UI ohne Deployment, integrierte Metrics und Feature-Adoption-Analytics, Audit-Log für Compliance, Flag-Governance mit Approvals. Das alles selbst zu bauen kostet Monate.

SDK Setup (Node.js)

// launchdarkly.client.ts import * as ld from '@launchdarkly/node-server-sdk'; let ldClient: ld.LDClient; export async function initLaunchDarkly(): Promise<void> { ldClient = ld.init(process.env.LD_SDK_KEY!, { logger: ld.basicLogger({ level: 'warn' }), // Offline-Fallback: nutzt Last-Known-Good Flags offline: process.env.NODE_ENV === 'test', // Streaming für Echtzeit-Updates stream: true, }); await ldClient.waitForInitialization({ timeout: 5 }); console.log('✅ LaunchDarkly SDK initialisiert'); } export function getLDClient(): ld.LDClient { if (!ldClient) throw new Error('LaunchDarkly nicht initialisiert!'); return ldClient; }

User Targeting und Gradual Rollout

// feature-service.ts — LaunchDarkly User Targeting import { getLDClient } from './launchdarkly.client'; interface LDUser { kind: 'user'; key: string; // userId — PFLICHT email?: string; name?: string; custom?: Record<string, string | boolean | number>; } export async function checkFeature( flagKey: string, user: LDUser, defaultValue = false ): Promise<boolean> { const client = getLDClient(); return client.variation(flagKey, user, defaultValue); } // Gradual Rollout: LaunchDarkly UI steuert Prozentsatz // Zuerst 1% → 5% → 25% → 50% → 100% — jederzeit pausierbar const context: LDUser = { kind: 'user', key: user.id, email: user.email, custom: { plan: user.plan, // 'free' | 'pro' | 'enterprise' betaUser: user.isBeta, // Beta-Targeting signupDate: user.createdAt.toISOString(), country: user.country, } }; const showNewFeature = await checkFeature('new-ai-dashboard', context); // Multivariate Flag: Rückgabewert ist String const client = getLDClient(); const layout = await client.variation( 'dashboard-layout', context, 'default' // Fallback ); // → 'default' | 'sidebar' | 'topnav' | 'compact'

Echtzeit-Updates via Streaming

// Flag-Änderungen sofort ohne Server-Neustart reaktieren const client = getLDClient(); client.on('update', (settings) => { // Wird aufgerufen wenn sich ein Flag in der LD-UI ändert console.log(`Flag geändert: ${settings.key}`); // Cache invalidieren, Metrics loggen, etc. flagUpdateMetrics.increment({ flag: settings.key }); }); // Graceful Shutdown process.on('SIGTERM', async () => { await client.close(); });

A/B Testing Pattern: Zufällige Zuweisung mit konsistenter User-Experience

A/B Testing über Feature Flags ist mächtig — aber nur wenn die Zuweisung deterministisch ist. Ein Nutzer muss bei jedem Request dieselbe Variante sehen, nicht zufällig wechseln. Das erreicht man durch Hash-basierte Zuweisung statt Math.random().

A/B Testing

Das Kernprinzip: Sticky Bucketing

Deterministischer Hash aus userId + flagKey → immer dieselbe Bucket-Nummer → immer dieselbe Variante. Auch nach Deployment, Server-Neustart oder Cache-Miss ist die Zuweisung identisch.

// ab-testing.service.ts — A/B Test mit Metrics-Erfassung import { createHash } from 'crypto'; type ABVariant = 'control' | 'variant-a' | 'variant-b'; interface ABExperiment { experimentKey: string; variants: { name: ABVariant; weight: number; // Summe aller Weights = 100 }[]; } export class ABTestingService { constructor( private metrics: MetricsService, private featureFlags: FeatureFlagService ) {} async getVariant( experimentKey: string, userId: string, experiment: ABExperiment ): Promise<ABVariant> { // Erst prüfen ob Experiment aktiv (Feature Flag) const isActive = await this.featureFlags.isEnabled( `experiment-${experimentKey}`, { userId } ); if (!isActive) return 'control'; // Deterministischer Bucket 0-99 const hash = createHash('sha256') .update(`${userId}:${experimentKey}`) .digest('hex'); const bucket = parseInt(hash.slice(0, 8), 16) % 100; // Variante anhand kumulativer Weights zuweisen let cumulative = 0; for (const variant of experiment.variants) { cumulative += variant.weight; if (bucket < cumulative) { // Zuweisung in Metrics loggen (einmalig) this.metrics.track('ab_assignment', { experiment: experimentKey, variant: variant.name, userId, }); return variant.name; } } return 'control'; } // Conversion Event tracken trackConversion(experimentKey: string, variant: ABVariant, userId: string) { this.metrics.track('ab_conversion', { experiment: experimentKey, variant, userId, timestamp: new Date().toISOString(), }); } } // Verwendung im Controller const variant = await abService.getVariant('checkout-cta', userId, { experimentKey: 'checkout-cta', variants: [ { name: 'control', weight: 50 }, // 50% "Jetzt kaufen" { name: 'variant-a', weight: 30 }, // 30% "Kostenlos starten" { name: 'variant-b', weight: 20 }, // 20% "30 Tage testen" ] }); // Auf Conversion tracken if (checkoutCompleted) { abService.trackConversion('checkout-cta', variant, userId); }
Wichtig bei A/B Tests: Tracke Assignments nur einmal pro Nutzer pro Experiment (Deduplizierung!). Sonst verfälschst du deine Stichprobe. Idealerweise verwendest du eine separate Assignment-Tabelle in deiner DB, die sicherstellt dass ein Nutzer immer dieselbe Variante bekommt — auch wenn der Cache geleert wird.

Vergleich: Eigenlösung vs. OpenFeature vs. LaunchDarkly

Feature Eigenlösung OpenFeature + Redis LaunchDarkly
Setup-Aufwand Mittel (2-3h) Gering (1h) Sehr gering (<30min)
Echtzeit-Updates Nein (Cache TTL) Manuell (Pub/Sub) Ja (SSE Streaming)
A/B Testing UI Nein Nein Ja (vollständig)
Audit-Log Selbst implementieren Teilweise Ja (DSGVO-konform)
Kosten $0 (nur Infra) $0 (nur Infra) Ab ~$10/Mo
Vendor Lock-in Keiner Keiner Hoch
Enterprise Features Nein Teilweise Ja

Best Practices: Flag Lifecycle, Stale Flags und Testing

Feature Flags sind wie technische Schulden: Wenn du sie nicht aktiv verwaltest, werden sie zum Problem. Stale Flags (nicht mehr benötigte Flags) verlangsamen den Code, verwirren das Team und können zu Sicherheitsproblemen führen.

Flag Lifecycle Management

1

Erstellen mit Ablaufdatum

Jeder Release-Flag bekommt bei Erstellung ein expires_at. Trag es direkt im Ticket ein. Ohne Ablaufdatum kein Flag erstellen.

2

Monitoring in Produktion

Dashboard zeigt alle aktiven Flags mit Alter. Flags älter als 30 Tage werden orange markiert — das Team muss Stellung nehmen: entfernen oder verlängern.

3

Rollout abschließen

Feature ist auf 100% und stabil seit >1 Woche → Flag aus dem Code entfernen (nicht nur auf enabled: true setzen). Pull Request: "Remove feature flag new-checkout".

4

Cleanup automatisieren

Wöchentlicher Cron listet alle expired Flags und erstellt automatisch GitHub Issues. Kein manuelles Tracking nötig.

Stale Flags automatisch finden

-- Stale Flag Report: Flags ohne Änderung seit 30+ Tagen SELECT flag_key, enabled, rollout_pct, AGE(updated_at) AS last_changed, expires_at, CASE WHEN expires_at < NOW() THEN 'EXPIRED' WHEN updated_at < NOW() - INTERVAL '30 days' THEN 'STALE' ELSE 'ACTIVE' END AS status FROM feature_flags WHERE updated_at < NOW() - INTERVAL '30 days' OR expires_at < NOW() ORDER BY last_changed DESC;

Testing mit Feature Flags: Alle Code-Pfade abdecken

Feature Flags verdoppeln deine Code-Pfade. Wenn du nicht explizit testest, hast du toten Code der sich unbemerkt bricht. Claude Code schreibt Tests für beide Zustände automatisch.

// feature-flags.test.ts — Beide Pfade testen import { describe, it, expect, vi, beforeEach } from 'vitest'; import { FeatureFlagService } from './feature-flags.service'; import { CheckoutService } from './checkout.service'; describe('CheckoutService — Feature Flag Varianten', () => { let flagService: FeatureFlagService; let checkoutService: CheckoutService; beforeEach(() => { flagService = { isEnabled: vi.fn() } as any; checkoutService = new CheckoutService(flagService); }); it('zeigt alten Checkout wenn Flag disabled', async () => { vi.mocked(flagService.isEnabled).mockResolvedValue(false); const result = await checkoutService.getCheckoutFlow(mockUser); expect(result.version).toBe('legacy'); expect(result.components).toContain('OldPaymentForm'); }); it('zeigt neuen Checkout wenn Flag enabled', async () => { vi.mocked(flagService.isEnabled).mockResolvedValue(true); const result = await checkoutService.getCheckoutFlow(mockUser); expect(result.version).toBe('new-2026'); expect(result.components).toContain('StripePaymentElement'); }); it('fällt auf legacy zurück wenn FlagService wirft', async () => { // Fehlerfall: Flag-Service nicht erreichbar vi.mocked(flagService.isEnabled).mockRejectedValue(new Error('Redis down')); const result = await checkoutService.getCheckoutFlow(mockUser); // IMMER sicher fallback auf bekannten Zustand expect(result.version).toBe('legacy'); }); it('Gradual Rollout: userId in 5% Bucket erhält neues Feature', async () => { // userId der deterministisch in ersten 5% fällt const userId = 'user-low-hash'; // In Tests: vorab berechnen vi.mocked(flagService.isEnabled).mockImplementation( async (key, ctx) => ctx?.userId === userId ? true : false ); const result = await checkoutService.getCheckoutFlow({ id: userId }); expect(result.version).toBe('new-2026'); }); });
Claude Code Test-Strategie: Erkläre Claude Code deinen Feature Flag Namen und welche Code-Pfade dahinter stehen — es generiert automatisch Tests für enabled/disabled/error-fallback-Szenarien. Mit Vitest und vi.fn() ist das in Minuten erledigt.

Kill Switch: Emergency Stop in Sekunden

// kill-switch.ts — Emergency Feature Deaktivierung import { FeatureFlagService } from './feature-flags.service'; export async function emergencyDisable( flagKey: string, reason: string ): Promise<void> { const service = new FeatureFlagService(); // 1. Flag in DB sofort deaktivieren await service.pg.query( `UPDATE feature_flags SET enabled = false, updated_at = NOW() WHERE flag_key = $1`, [flagKey] ); // 2. Redis Cache sofort invalidieren (kein 60s Warten!) await service.redis.del(`ff:${flagKey}`); // 3. Pub/Sub Broadcast für alle Instanzen await service.redis.publish('flag-updates', JSON.stringify({ action: 'DISABLE', flagKey, reason, timestamp: new Date() })); // 4. Alert senden console.error(`🚨 KILL SWITCH aktiviert: ${flagKey} — ${reason}`); } // CLI-Usage bei Incident // npx ts-node kill-switch.ts new-ai-dashboard "Performance Degradation P1"

Feature Flag Checkliste: Dos and Don'ts

  • Jeder Release-Flag hat ein expires_at Datum beim Erstellen
  • Default-Wert im Code ist immer false (nicht true) — Safety-First
  • Feature-Flag-Name enthält den Ticket/Issue-Key: PROJ-123-new-checkout
  • Beide Code-Pfade (flag=true und flag=false) haben eigene Tests
  • Fehler im Flag-Service → immer Fallback auf Default, niemals Exception nach oben
  • Gradual Rollout: Hash-basiert deterministisch, nie Math.random()
  • Flag nach vollständigem Rollout aus Codebase entfernen (nicht nur enableden)
  • Kill Switch Pattern: Redis-Cache sofort invalidieren, nicht auf TTL warten
  • Wöchentlicher Stale-Flag-Report als automatischer Cron
  • Flag-Änderungen in Audit-Log mit Zeitstempel, User und Reason
Flag-Explosion vermeiden: Mehr als 50 aktive Flags gleichzeitig ist ein Warnsignal. Jeder neue Flag muss den "Brauchen wir das wirklich?"-Check bestehen. Permissions-Flags für dauerhafte Tier-Unterschiede sind ok — aber Release-Flags müssen konsequent geklint werden.

Feature Flags in deinem Projekt implementieren?

Claude Code analysiert deine bestehende Architektur und schlägt die passende Feature-Flag-Strategie vor — von der einfachen Eigenlösung bis zur LaunchDarkly-Integration. Teste 14 Tage kostenlos.

14 Tage kostenlos testen →

Fazit: Feature Flags sind kein Nice-to-Have

Teams die Trunk-Based Development mit Feature Flags praktizieren, deployen 5-10× häufiger als Teams mit Feature-Branches — und haben weniger Incidents, weil Rollouts graduell und reversibel sind. Der Kill Switch allein ist den Aufwand wert.

Der Einstieg ist einfach: Fang mit einem einfachen Redis-backed Service an, nutze OpenFeature für die Abstraktion, und füge LaunchDarkly hinzu wenn die Komplexität es rechtfertigt. Claude Code hilft dir beim Setup — in der Zeit die du sonst für Merge-Konflikte verbrätst.