Zurück zum Blog
Jan. 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

Feature-Flags: Ein praktischer Leitfaden

#backend#devops#engineering-practice

Ein Feature-Flag ist eine Bedingung um ein Stück Code, mit der man diesen Code ein- oder ausschalten kann – für alle, für eine Teilmenge der Nutzer oder schrittweise – ohne ein neues Deployment auszuliefern. Die Idee ist so einfach, dass es verlockend ist, einfach if-Anweisungen durch die Codebasis zu streuen, und genau so laufen Feature-Flags im großen Maßstab schief. Gut gemacht entkoppeln sie das Deployen von Code vom Freigeben; nachlässig eingesetzt werden sie zu einem dauerhaften Labyrinth aus toten Zweigen, die sich niemand zu löschen traut.

Die einfachstmögliche Version

Im Kern ist ein Feature-Flag nur eine boolesche Prüfung:

if (flags.isEnabled('new-checkout-flow', { userId: user.id })) {
  return renderNewCheckout();
}
return renderLegacyCheckout();

Das Interessante ist nicht das if – es ist flags.isEnabled. Woher kommt diese Antwort, und wie schnell kann sie sich ändern?

Drei Wege, ein Flag auszuwerten

Statische Konfiguration. Eine JSON- oder YAML-Datei oder Umgebungsvariablen, geprüft zur Deploy-Zeit. Am einfachsten umzusetzen, aber ein Flag zu ändern bedeutet ein Redeployment – was einen großen Teil des Sinns (schnelles, risikoarmes Umschalten) zunichtemacht.

const flags = { 'new-checkout-flow': process.env.FF_NEW_CHECKOUT === 'true' };

Ein Remote-Flag-Dienst, der zur Laufzeit abgefragt (und gecacht) wird, sodass ein Flag von einem Dashboard aus umgelegt werden kann, ohne die Deploy-Pipeline überhaupt anzufassen:

async function isEnabled(flagKey, context) {
  const cached = cache.get(flagKey);
  if (cached) return evaluate(cached, context);

  const flag = await flagService.fetch(flagKey);
  cache.set(flagKey, flag, { ttl: 30_000 });
  return evaluate(flag, context);
}

Prozentuale oder attributbasierte Ausrollung, bei der das Flag nicht nur an/aus ist, sondern einen Teil des Traffics anspricht – 5 % der Nutzer oder alle mit plan === 'enterprise':

function evaluate(flag, context) {
  if (!flag.enabled) return false;
  if (flag.rolloutPercent != null) {
    const bucket = hashToPercent(context.userId, flag.key);
    return bucket < flag.rolloutPercent;
  }
  if (flag.rules) {
    return flag.rules.every((rule) => matchesRule(rule, context));
  }
  return true;
}

function hashToPercent(userId, flagKey) {
  const hash = simpleHash(`${userId}:${flagKey}`);
  return hash % 100;
}

Der hashToPercent-Teil ist wichtiger, als er aussieht: Hashen auf userId (statt pro Anfrage zu würfeln) bedeutet, dass derselbe Nutzer über Seitenaufrufe hinweg konsistent auf derselben Seite des Flags landet – sonst könnte ein Nutzer bei einer Anfrage den neuen Checkout sehen und bei der nächsten den alten, was verwirrend ist und Bugs nahezu unreproduzierbar macht.

Wofür Flags tatsächlich da sind

Release-Flags entkoppeln Deploy von Release: Code unsichtbar mergen und ausliefern, dann einschalten, wenn man bereit ist, unabhängig vom nächsten Deploy-Fenster. Experiment-Flags führen einen A/B-Test durch – ein Prozentsatz der Nutzer bekommt Variante B, und man misst den Unterschied. Ops-Flags sind Notausschalter für kontrollierte Degradierung – ein langsames, nicht kritisches Feature (Empfehlungen, ein Drittanbieter-Widget) unter Last ohne Deploy abschalten. Berechtigungs-Flags schalten ein Feature für bestimmte Nutzer oder Tarife frei, etwa ein Enterprise-only-Feature – wobei für langlebige Zugriffssteuerung ein richtiges Berechtigungssystem meist besser dient als ein Flag, das eigentlich vorübergehend gedacht war.

Diese Unterscheidung ist wichtig, weil jeder Typ einen anderen Lebenszyklus hat. Ops-Flags können ewig leben. Release- und Experiment-Flags sollten es nicht.

Der Teil, den alle überspringen: sie löschen

Ein Flag, das seit sechs Monaten bei 100 % steht und für das kein Codepfad mehr existiert, der das alte Verhalten prüft, bietet keine Sicherheit mehr – es ist toter Ballast, der die Codebasis schwerer lesbar und jede künftige Änderung riskanter macht, weil es jetzt zwei Pfade zum Nachdenken gibt statt einen.

// Six months later, this still exists:
if (flags.isEnabled('new-checkout-flow')) {
  return renderNewCheckout();
}
return renderLegacyCheckout(); // dead code nobody has run in months, still shipped, still maintained

Behandle das Aufräumen von Flags als Teil der Definition of Done eines Features, nicht als optionale Nacharbeit: Sobald ein Flag vollständig ausgerollt und eine angemessene Zeit stabil ist, entferne die Flag-Prüfung und den toten Zweig im selben Durchgang. Manche Teams erzwingen das mit einem Linter, der jede Flag-Prüfung markiert, die älter als N Tage ist, oder mit einem Dashboard, das Flags zeigt, die lange niemand umgelegt hat.

Ein Fallstrick: Flags, die zusammenwirken

Zwei scheinbar unabhängige Flags können sich zu einem Zustand kombinieren, den niemand getestet hat:

if (flags.isEnabled('new-checkout-flow')) {
  if (flags.isEnabled('new-payment-provider')) {
    return renderNewCheckoutWithNewPayments(); // did anyone actually test this combination?
  }
  return renderNewCheckoutWithOldPayments();
}

Jedes zusätzliche Flag verdoppelt die Anzahl der Codepfade in den Teilen der App, die es berührt. Zwei Flags sind vier Pfade; drei sind acht. Halte Flags eng gefasst und kurzlebig, und sei bewusst darin, welche Kombinationen du tatsächlich unterstützen musst – jede Kombination zu testen ist unrealistisch, sobald mehr als ein paar Flags im selben Codebereich aktiv sind.

Fazit

Feature-Flags geben dir die Möglichkeit, Code auszuliefern, ohne ihn freizugeben, schrittweise auszurollen und ein fehlerhaftes Feature ohne Deploy abzuschalten – wirklich wertvolle Eigenschaften. Der Fehlerfall ist nicht das Flag selbst, sondern es als dauerhaft statt vorübergehend zu behandeln: Jedes Flag, das du hinzufügst, ist eine Schuld, die durch spätere Entfernung beglichen werden muss, und eine Codebasis, die diese Schuld nie zurückzahlt, ist am Ende schwerer zu durchdringen als eine ganz ohne Flags.

NeuerDas Circuit-Breaker-Muster für widerstandsfähige DiensteÄlterTypeScript-Generics verstehen