Feature Flags im großen Maßstab: Progressive Delivery
Baue ein Feature-Flag-System mit prozentualen Rollouts, Nutzersegmentierung, A/B-Tests und Kill Switches, das Deployment und Release entkoppelt.

Deployment ist nicht Release
Code deployen und ein Feature releasen sind zwei unterschiedliche Aktionen. Deployment ist ein technischer Vorgang — Artefakte in die Produktion zu bringen. Release ist eine Geschäftsentscheidung — Funktionalität für Nutzer freizuschalten. Feature Flags trennen beides: Du kannst jederzeit inaktiven Code deployen und Features freigeben, wann und für wen du willst.
Der Feature-Flag-Vertrag
Ein Feature Flag ist ein Entscheidungspunkt zur Laufzeit. Im einfachsten Fall ist es ein Boolean. Im großen Maßstab ist es eine Rule Engine, die den Nutzerkontext anhand von Targeting-Regeln auswertet und einen Wert zurückgibt.
interface FeatureFlag {
key: string;
type: "boolean" | "string" | "number" | "json";
defaultValue: unknown;
enabled: boolean;
rules: TargetingRule[];
killSwitch: boolean;
}
interface TargetingRule {
priority: number;
conditions: Condition[];
percentage: number;
value: unknown;
}
interface Condition {
attribute: string;
operator: "eq" | "neq" | "in" | "contains" | "gt" | "lt" | "semver_gt";
value: unknown;
}
interface EvaluationContext {
userId: string;
email?: string;
plan?: string;
country?: string;
appVersion?: string;
customAttributes?: Record<string, unknown>;
}class FeatureFlagClient {
private flags: Map<string, FeatureFlag> = new Map();
evaluate<T>(flagKey: string, context: EvaluationContext, fallback: T): T {
const flag = this.flags.get(flagKey);
// Flag doesn't exist — return fallback
if (!flag) return fallback;
// Kill switch activated — return default immediately
if (flag.killSwitch) return flag.defaultValue as T;
// Flag disabled globally — return default
if (!flag.enabled) return flag.defaultValue as T;
// Evaluate targeting rules in priority order
const sortedRules = [...flag.rules].sort(
(a, b) => a.priority - b.priority
);
for (const rule of sortedRules) {
if (this.matchesConditions(rule.conditions, context)) {
if (this.isInPercentage(context.userId, flagKey, rule.percentage)) {
return rule.value as T;
}
}
}
return flag.defaultValue as T;
}
private matchesConditions(
conditions: Condition[],
context: EvaluationContext
): boolean {
return conditions.every((condition) => {
const contextValue = this.getAttribute(context, condition.attribute);
switch (condition.operator) {
case "eq":
return contextValue === condition.value;
case "neq":
return contextValue !== condition.value;
case "in":
return Array.isArray(condition.value) &&
condition.value.includes(contextValue);
case "contains":
return typeof contextValue === "string" &&
contextValue.includes(condition.value as string);
default:
return false;
}
});
}
// Deterministic percentage based on userId + flagKey hash
private isInPercentage(
userId: string,
flagKey: string,
percentage: number
): boolean {
if (percentage >= 100) return true;
if (percentage <= 0) return false;
const hash = this.consistentHash(`${userId}:${flagKey}`);
return (hash % 100) < percentage;
}
}Muster für progressive Rollouts
Starte mit internen Nutzern, erweitere auf Beta-Tester und fahre dann auf 1 %, 5 %, 25 %, 50 %, 100 % hoch. Wenn die Metriken in irgendeiner Phase schlechter werden, stoppe sofort oder rolle zurück — ohne Deployment.
// ❌ Big-bang release — all users get the feature at once
function NewCheckout() {
return <RadicallyDifferentCheckout />;
}
// ✅ Progressive rollout with monitoring at each stage
function Checkout() {
const flags = useFeatureFlags();
const isNewCheckout = flags.evaluate("new-checkout-flow", {
userId: user.id,
plan: user.plan,
country: user.country,
}, false);
return isNewCheckout ? <NewCheckoutFlow /> : <CurrentCheckoutFlow />;
}
// Rollout configuration evolution:
const rolloutStages: TargetingRule[] = [
// Stage 1: Internal team only
{
priority: 1,
conditions: [{ attribute: "email", operator: "contains", value: "@ourcompany.com" }],
percentage: 100,
value: true,
},
// Stage 2: Beta users
{
priority: 2,
conditions: [{ attribute: "plan", operator: "eq", value: "beta" }],
percentage: 100,
value: true,
},
// Stage 3: 5% of all users
{
priority: 3,
conditions: [],
percentage: 5,
value: true,
},
// Stage 4: 50% of all users
// Stage 5: 100% — flag cleaned up
];Serverseitige vs. clientseitige Auswertung
Wo du Flags auswertest, beeinflusst Latenz, Sicherheit und Zuverlässigkeit. Serverseitige Auswertung hält Targeting-Regeln privat und sorgt für Konsistenz. Clientseitige Auswertung reduziert Latenz für UI-Änderungen, macht aber Flag-Konfigurationen sichtbar.
// Server-side: evaluate on the API, send only the result
// app/api/flags/route.ts
export async function GET(request: Request) {
const context = await buildContextFromRequest(request);
// Only send evaluated values, never rules or conditions
const flags = {
"new-checkout": flagClient.evaluate("new-checkout", context, false),
"dark-mode": flagClient.evaluate("dark-mode", context, false),
"pricing-tier": flagClient.evaluate("pricing-tier", context, "standard"),
};
return Response.json(flags);
}
// Client-side: bootstrap with server-evaluated values
function FlagProvider({ children }: { children: React.ReactNode }) {
const [flags, setFlags] = useState<Record<string, unknown>>({});
useEffect(() => {
fetch("/api/flags")
.then((r) => r.json())
.then(setFlags);
}, []);
return (
<FlagContext.Provider value={flags}>
{children}
</FlagContext.Provider>
);
}
function useFlag<T>(key: string, fallback: T): T {
const flags = useContext(FlagContext);
return (flags[key] as T) ?? fallback;
}Flag-Lebenszyklus und technische Schulden
Feature Flags, die ewig leben, werden zu technischen Schulden. Jedes Flag ist ein Zweig in deinem Code. Etabliere einen Lebenszyklus: erstellen, ausrollen, 100 % erreichen, Flag und alten Codepfad entfernen.
interface FlagMetadata {
key: string;
owner: string;
createdAt: string;
expectedRemovalDate: string;
purpose: "release" | "experiment" | "ops" | "permission";
jiraTicket: string;
}
// Automated flag staleness detection
function findStaleFlags(
flags: FlagMetadata[],
today: Date = new Date()
): FlagMetadata[] {
return flags.filter((flag) => {
const removalDate = new Date(flag.expectedRemovalDate);
return removalDate < today;
});
}
// ESLint rule concept: warn on flags past their removal date
// Flag inventory tracked alongside code
const FLAG_REGISTRY: FlagMetadata[] = [
{
key: "new-checkout-flow",
owner: "payments-team",
createdAt: "2025-06-01",
expectedRemovalDate: "2025-08-01",
purpose: "release",
jiraTicket: "PAY-1234",
},
{
key: "maintenance-mode",
owner: "platform-team",
createdAt: "2025-01-15",
expectedRemovalDate: "9999-12-31", // Permanent ops flag
purpose: "ops",
jiraTicket: "PLAT-500",
},
];Monitoring und Observability
Jede Flag-Auswertung sollte Telemetrie emitieren. Ohne sie kannst du Flag-Zustände nicht mit Fehlerraten, Latenzänderungen oder Geschäftsmetriken korrelieren.
class ObservableFeatureFlagClient extends FeatureFlagClient {
constructor(private readonly metrics: MetricsClient) {
super();
}
evaluate<T>(flagKey: string, context: EvaluationContext, fallback: T): T {
const start = performance.now();
const result = super.evaluate(flagKey, context, fallback);
const duration = performance.now() - start;
this.metrics.increment("feature_flag.evaluation", {
flag: flagKey,
result: String(result),
userId: context.userId,
});
this.metrics.histogram("feature_flag.evaluation_ms", duration, {
flag: flagKey,
});
return result;
}
}Wichtige Erkenntnisse
Feature Flags entkoppeln Deployment und Release. Code jederzeit deployen; Features für bestimmte Nutzer, in bestimmten Prozentsätzen, per Schalter freigeben — nicht per Deployment. Nutze progressive Rollouts, um den Blast Radius zu begrenzen: zuerst intern, dann Beta, dann Prozentsätze hochfahren mit Monitoring in jeder Phase.
Werte Flags serverseitig aus, wenn Targeting-Regeln sensibel sind oder Konsistenz wichtig ist. Tracke jedes Flag mit Metadaten — Owner, Zweck, geplantes Entfernungsdatum — und erzwinge die Bereinigung, um technische Schulden zu vermeiden. Emitiere Telemetrie bei jeder Auswertung, damit du Flag-Zustände mit Produktionsmetriken korrelieren kannst.
Das Ziel ist nicht, mehr Flags zu haben. Das Ziel ist, mit Vertrauen auszuliefern und zu wissen, dass jedes neue Feature in Sekunden zurückgedreht werden kann, ohne die Deployment-Pipeline anzufassen.


