Zum Inhalt springen

Feature Flags im großen Maßstab

Feature Flags entkoppeln Deployment und Release — so setzt du sie um, ohne ein unentwirrbares Geflecht aus bedingter Logik zu erzeugen.

3 Min. Lesezeit
Entscheidungsbaum für Feature Flags, der zeigt, wie Flags Codepfade in der Produktion steuern

Feature Flags sind eines der mächtigsten Werkzeuge in der modernen Softwarebereitstellung. Sie erlauben es dir, unvollständige Features in Produktion zu deployen, A/B-Tests durchzuführen, Änderungen schrittweise auszurollen und problematische Features sofort abzuschalten — ohne neu zu deployen. Aber ohne Disziplin werden Flags zu technischer Schuld, die jeden Codepfad schwerer nachvollziehbar macht.

Die einfachste Implementierung

Im Kern ist ein Feature Flag eine Bedingung. Die Frage ist, woher der Flag-Wert kommt und wie er verwaltet wird.

tstypescript
// ❌ Hard-coded flag — requires redeployment to change
const NEW_CHECKOUT = true;
 
if (NEW_CHECKOUT) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}
 
// ✅ Runtime flag — changeable without deployment
const flags = await getFeatureFlags(userId);
 
if (flags.isEnabled("new-checkout")) {
  renderNewCheckout();
} else {
  renderLegacyCheckout();
}

Runtime-Flags sind das Ziel. Sie lassen sich in Sekunden über ein Dashboard umschalten, verglichen mit Minuten oder Stunden für einen Deployment.

Flag-Typen

Nicht alle Flags dienen demselben Zweck. Ihre Kategorisierung bestimmt Lebenszyklus und Aufräumstrategie.

TypZweckLebensdauerBeispiel
Release-FlagUnvollständige Features gattenTage bis Wochennew-checkout
Experiment-FlagA/B-TestsWochen bis Monatecheckout-redesign-v2
Ops-FlagKill Switch für LastDauerhaftdisable-search-indexing
Berechtigungs-FlagFeatures nach NutzerstufeDauerhaftpremium-analytics

Release-Flags sollten kurzlebig sein und aggressiv aufgeräumt werden. Experiment-Flags leben während der Testphase. Ops- und Berechtigungs-Flags sind dauerhafte Infrastruktur.

Einen Flag-Service bauen

tstypescript
interface FeatureFlag {
  key: string;
  enabled: boolean;
  rolloutPercentage: number;
  allowedUserIds: string[];
  rules: FlagRule[];
}
 
interface FlagRule {
  attribute: string;
  operator: "eq" | "in" | "gt" | "lt";
  value: unknown;
}
 
class FeatureFlagService {
  private flags: Map<string, FeatureFlag>;
 
  constructor(flags: FeatureFlag[]) {
    this.flags = new Map(flags.map((f) => [f.key, f]));
  }
 
  isEnabled(key: string, context: FlagContext): boolean {
    const flag = this.flags.get(key);
    if (!flag) return false;
    if (!flag.enabled) return false;
 
    // Explicit allow list
    if (flag.allowedUserIds.includes(context.userId)) return true;
 
    // Rule-based targeting
    if (flag.rules.length > 0) {
      return flag.rules.every((rule) => this.evaluateRule(rule, context));
    }
 
    // Percentage rollout — deterministic per user
    if (flag.rolloutPercentage < 100) {
      const hash = this.hashUserForFlag(context.userId, key);
      return hash < flag.rolloutPercentage;
    }
 
    return true;
  }
 
  private hashUserForFlag(userId: string, flagKey: string): number {
    // Deterministic hash — same user always gets same result
    let hash = 0;
    const input = `${userId}:${flagKey}`;
    for (let i = 0; i < input.length; i++) {
      hash = (hash * 31 + input.charCodeAt(i)) | 0;
    }
    return Math.abs(hash) % 100;
  }
 
  private evaluateRule(rule: FlagRule, context: FlagContext): boolean {
    const value = context[rule.attribute];
    switch (rule.operator) {
      case "eq": return value === rule.value;
      case "in": return (rule.value as unknown[]).includes(value);
      case "gt": return (value as number) > (rule.value as number);
      case "lt": return (value as number) < (rule.value as number);
      default: return false;
    }
  }
}

Das hashbasierte prozentuale Rollout ist entscheidend: Es stellt sicher, dass ein Nutzer konsistent dieselbe Variante sieht, ohne Zuordnungen zu speichern. Erhöhe den Prozentsatz, um schrittweise mehr Nutzer freizuschalten.

Schrittweise Rollouts

Der sicherste Weg, ein Feature zu releasen, ist schrittweise: zuerst interne Nutzer, dann ein kleiner Prozentsatz, dann weiter.

tstypescript
// Rollout strategy for a major feature
const rolloutStages = [
  { percentage: 0, allowList: ["internal-team-ids"], duration: "2 days" },
  { percentage: 5, allowList: [], duration: "3 days" },
  { percentage: 25, allowList: [], duration: "2 days" },
  { percentage: 50, allowList: [], duration: "2 days" },
  { percentage: 100, allowList: [], duration: "permanent" },
];
tstypescript
// Monitor error rates at each stage
function shouldProceedToNextStage(metrics: StageMetrics): boolean {
  const errorRateThreshold = 0.01; // 1%
  const latencyP99Threshold = 500; // ms
 
  return (
    metrics.errorRate < errorRateThreshold &&
    metrics.latencyP99 < latencyP99Threshold &&
    metrics.userComplaints === 0
  );
}

Wenn die Fehlerrate bei 5% hochschießt, rollst du sofort auf 0% zurück — kein Deployment, kein Downtime.

Das Aufräum-Problem

Das größte Risiko bei Feature Flags ist nicht das Feature, sondern die Flags selbst. Veraltete Flags sammeln sich an und erzeugen eine nicht testbare kombinatorische Explosion.

tstypescript
// ❌ Stale flags — nobody knows if these are still needed
if (flags.isEnabled("new-checkout")) {
  if (flags.isEnabled("checkout-v2-variant-b")) {
    if (flags.isEnabled("express-checkout")) {
      // How many code paths is this? 2^3 = 8 combinations to test
    }
  }
}
 
// ✅ Clean up flags after full rollout
// 1. Roll out to 100%
// 2. Monitor for 1 week
// 3. Remove the flag check, keep only the new code path
// 4. Delete the flag from the configuration

Füge Release-Flags Ablaufdaten hinzu. Verfolge das Alter von Flags in deinem Dashboard. Ein Release-Flag, das älter als 30 Tage ist, ist technische Schuld.

Testen mit Flags

Feature Flags erschweren das Testen. Du musst beide Pfade testen — und idealerweise den Übergang zwischen ihnen.

tstypescript
describe("checkout", () => {
  it("renders new checkout when flag is enabled", () => {
    const flags = new FeatureFlagService([
      { key: "new-checkout", enabled: true, rolloutPercentage: 100, allowedUserIds: [], rules: [] },
    ]);
 
    const result = renderCheckout(flags, testContext);
    expect(result).toContain("Express Checkout");
  });
 
  it("renders legacy checkout when flag is disabled", () => {
    const flags = new FeatureFlagService([
      { key: "new-checkout", enabled: false, rolloutPercentage: 0, allowedUserIds: [], rules: [] },
    ]);
 
    const result = renderCheckout(flags, testContext);
    expect(result).toContain("Standard Checkout");
  });
});

Wichtige Erkenntnisse

  1. Feature Flags entkoppeln Deployment und Release — deploye täglich, release, wenn es bereit ist
  2. Kategorisiere Flags nach Typ — Release-Flags sind temporär, Ops-Flags sind dauerhaft
  3. Schrittweise Rollouts reduzieren das Risiko — starte bei 0%, erhöhe basierend auf Metriken
  4. Hashbasiertes prozentuales Rollout stellt sicher, dass Nutzer konsistent dieselbe Variante sehen
  5. Räume Flags aggressiv auf — veraltete Flags erzeugen nicht testbare kombinatorische Explosionen
  6. Jedes Flag braucht einen Eigner und ein Ablaufdatum für Release-Flags
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX