Feature Flags für SaaS: Progressive Delivery ohne Rollback

#Feature Flags SaaS
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Feature Flags für SaaS sind eines jener Muster, das jedes Engineering-Team irgendwann für sich entdeckt - meistens direkt nach dem ersten schmerzhaften nächtlichen Rollback. Die Idee ist verführerisch: Code deployen, wann immer du willst, und ihn erst dann aktivieren, wenn du bereit bist. Kein Binden von Releases an Sprint-Deadlines mehr. Keine "Merge-Freeze"-Mails mehr am Tag vor einer wichtigen Kundendemo.

Aber das Muster hat einen Preis, über den in den Tutorials selten gesprochen wird. Innerhalb von achtzehn Monaten nach leichtsinniger Flag-Einführung häufen die meisten Teams an, was Entwickler einen "Flag-Friedhof" nennen - Dutzende toter Boolean-Checks, über die Codebasis verstreut, jeder eine kleine kognitive Belastung für jeden Entwickler, der die Datei liest. Das Problem sind nicht Feature Flags an sich. Das Problem ist, sie als Deployment-Abkürzung zu behandeln statt als strukturierte Engineering-Praxis.

Dieser Leitfaden zeigt, wie man es richtig macht: die Taxonomie der Flag-Typen, die wirklich wichtig ist, wie man einen Lebenszyklus entwirft, der Aufräumarbeiten erzwingt, und wie man Progressive Delivery in Symfony und Next.js implementiert - ohne LaunchDarkly-Unternehmenspreise zu abonnieren.

Warum die meisten Teams Feature Flags falsch einsetzen

Das Scheitermuster ist über Codebasen hinweg konsistent, die ich im Laufe der Jahre auditiert habe. Ein Team beginnt mit einem Flag - vielleicht für eine riskante Datenbankmigrierung oder einen neuen Checkout-Flow. Es funktioniert gut, also kommen mehr hinzu. Nach sechs Monaten erinnert sich niemand mehr, welche Flags noch aktiv sind, welche verkleidete permanente Konfigurationseinstellungen sind und welche vor zwei Quartalen hätten auslaufen sollen.

Die Ursache ist ein Benennungs- und Kategorisierungsproblem. Wenn jedes Flag nur isNewDashboardEnabled oder featureX heißt, wird der Flag-Store undurchsichtig. Du kannst die Frage "Wozu dient dieses Flag eigentlich?" nicht beantworten, ohne den Code zu lesen, der es verwendet. Diese Ambiguität ist es, die Codebasen tötet - nicht die Flags selbst.

Die Lösung besteht darin, vor dem Erstellen eines Flags einen Flag-Typ zu wählen und diesen Typ in die Lifecycle-Verwaltung des Flags einzucodieren.

Die drei Flag-Typen, die wirklich wichtig sind

Die meisten Feature-Flag-Taxonomien sind zu kompliziert. Für das typische SaaS-Team decken drei Kategorien fast jeden realen Anwendungsfall ab.

Release-Flags sind temporär. Sie umhüllen Code, der noch nicht für alle Nutzer bereit ist - ein neuer Billing-Flow, eine überarbeitete Einstellungsseite, ein Performance-Experiment. Release-Flags sollten ein definiertes Ablaufdatum haben: entweder ein Datum oder eine Bedingung wie "nach 90 Tagen in Produktion entfernen". Wenn der Rollout abgeschlossen ist und der alte Code-Pfad verschwunden ist, muss das Flag gelöscht werden. Wenn du dich nicht verpflichten kannst, es zu löschen, solltest du es nicht erstellen.

Kill-Switches sind permanente Betriebskontrollen. Sie ermöglichen es, eine bestimmte Integration oder einen Feature-Pfad zu deaktivieren, ohne Code deployen zu müssen - nützlich für Drittanbieterdienste mit Ausfallzeiten, für Features, die unerwartete Last erzeugen, oder aus regulatorischen Gründen, die ein schnelles Abschalten erfordern. Kill-Switches leben dauerhaft im Flag-Store, werden aber immer als "an" erwartet. Wenn ein Kill-Switch "aus" ist, stimmt etwas nicht und das Team sollte es wissen.

Entitlement-Flags steuern, auf was verschiedene Kunden oder Pläne zugreifen können. Sie sind nicht wirklich Feature Flags im klassischen Sinne - sie sind Produktkonfiguration. Wenn dein SaaS einen kostenlosen und einen kostenpflichtigen Tarif hat, gehört der Unterschied zwischen den Möglichkeiten beider Tarife in Entitlement-Flags. Diese sind permanent und liegen in der Verantwortung des Produktteams, nicht des Engineering-Teams.

Diese Unterscheidung ist praktisch relevant: Release-Flags sollten regelmäßig überprüft und gelöscht werden, Kill-Switches sollten auf unerwartete Aus-Zustände überwacht werden, und Entitlement-Flags sollten in deiner Billing- oder Plan-Konfiguration leben, nicht in einem generischen Flag-Store.

Einen Flag-Lebenszyklus entwerfen

Ein Flag ohne Eigentümer und Ablaufdatum ist eine Haftung. Hier ist der minimale Lebenszyklus, der Friedhof-Ansammlungen verhindert.

Wenn ein Flag erstellt wird, müssen drei Dinge festgehalten werden: der Flag-Typ, der Eigentümer (der Entwickler oder das Team, das für das Aufräumen verantwortlich ist), und bei Release-Flags ein angestrebtes Entfernungsdatum. Das kann in Kommentaren, einer Datenbankspalte oder einer YAML-Definitionsdatei leben - wichtig ist, dass es aufgeschrieben und überprüft wird.

Jeden Sprint sollten Release-Flags, die ihr Zieldatum überschritten haben, auf dem Radar von jemandem erscheinen. Ein einfacher CI-Check, der den Build fehlschlagen lässt, wenn ein Flag sein Ablaufdatum überschritten hat, klingt hart, ist aber bemerkenswert effektiv. Der Schmerz, ein Datum zu aktualisieren, ist viel geringer als der Schmerz, versehentlich Code zu shippen, der von einem toten Flag abhängt.

Wenn ein Release-Flag ausgemustert wird, ist der Prozess: global aktivieren, einen vollen Deployment-Zyklus warten, um die Stabilität zu bestätigen, dann das Flag und alle alten Code-Pfade, die es geschützt hat, löschen. Lass das Flag nicht in einem "dauerhaft aktiviert"-Zustand - das ist nur eine andere Art von Friedhof.

Feature Flags in Symfony implementieren

Für eine Symfony-Anwendung erfordert der leichteste Ansatz, der tatsächlich in Produktion funktioniert, keinen Drittanbieterdienst. Ein datenbankbasierter Flag-Store mit einer dünnen Service-Schicht deckt die überwiegende Mehrheit der SaaS-Anforderungen ab.

Beginne mit einer FeatureFlag-Entity mit Spalten für name (String), enabled (Boolean), flag_type (Enum: release, kill_switch, entitlement), owner (String), expires_at (nullable Datetime) und context (JSON für Entitlement-Regeln). Füge einen Unique-Index auf name hinzu.

Die Service-Klasse ist unkompliziert:

// src/Service/FeatureFlagService.php
final class FeatureFlagService
{
    public function __construct(
        private readonly FeatureFlagRepository $repository,
        private readonly CacheInterface $cache,
    ) {}

    public function isEnabled(string $name, array $context = []): bool
    {
        $flag = $this->cache->get(
            'feature_flag_' . $name,
            fn() => $this->repository->findOneByName($name)
        );

        if (null === $flag) {
            return false; // fail closed - unbekannte Flags sind aus
        }

        if (!$flag->isEnabled()) {
            return false;
        }

        // Entitlement-Flags können Kontext-Regeln enthalten
        if ($flag->getFlagType() === FlagType::Entitlement && !empty($flag->getContext())) {
            return $this->evaluateEntitlementContext($flag->getContext(), $context);
        }

        return true;
    }
}

Ein paar Implementierungsentscheidungen, die explizit erwähnenswert sind. Der Service schlägt geschlossen fehl - wenn ein Flag nicht im Store existiert, gibt isEnabled false zurück. Das verhindert die stille Aktivierung von neuem Code, wenn ein Flag versehentlich fehlt. Ergebnisse werden pro Flag-Name gecacht; das Cache-TTL sollte bei Kill-Switches kurz (30-60 Sekunden) und bei stabilen Entitlement-Flags länger (5-10 Minuten) sein. Das Cache-Leeren bei Flag-Updates ist mit Symfonys Cache-Tagging unkompliziert.

Für Twig-Templates macht eine einfache Extension Flag-Checks lesbar:

{% if feature('new_billing_flow') %}
  {% include 'billing/_new_flow.html.twig' %}
{% else %}
  {% include 'billing/_legacy_flow.html.twig' %}
{% endif %}

Für die Admin-Verwaltung von Flags verarbeiten EasyAdmin oder SonataAdmin eine einfache CRUD-Oberfläche mit minimalem Konfigurationsaufwand. Stelle sie hinter deiner bestehenden Admin-Firewall bereit und du hast ein Feature-Flag-Dashboard, ohne eine eigene UI zu bauen.

Feature Flags in Next.js implementieren

Im Frontend ist die architektonische Wahl, wo Flags ausgewertet werden: am Edge, auf dem Server oder auf dem Client. Jeder Ansatz hat Kompromisse.

Edge-Auswertung (via Next.js Middleware) ist ideal für Kill-Switches und risikoreiche Release-Flags, bei denen du dir keinen Flash veralteter Inhalte leisten kannst. Middleware läuft vor dem Rendern der Seite, sodass du die Anfrage umleiten oder umschreiben kannst, bevor der Nutzer etwas sieht. Die Einschränkung ist, dass Middleware keinen Datenbankzugriff hat, sodass Flags aus einer Edge-kompatiblen Quelle kommen müssen - einem KV-Store wie Vercel KV, Cloudflare KV oder einer Redis-Instanz mit schnellem Lesepfad.

Server-Component-Auswertung funktioniert gut für die meisten Release-Flags und Entitlements. Den Flag-Status in der Server-Component abrufen und als Prop nach unten weitergeben. Kein Client-seitiger JavaScript-Overhead, kein Layout-Shift, kein Flash falscher Inhalte. Das ist der richtige Standard für neue Flags.

// app/billing/page.tsx
import { getFeatureFlag } from '@/lib/flags';

export default async function BillingPage() {
  const newBillingFlow = await getFeatureFlag('new_billing_flow');

  return newBillingFlow
    ? <NewBillingFlow />
    : <LegacyBillingFlow />;
}

Client-seitige Auswertung über einen React-Kontext ist nur für Flags sinnvoll, die sich während einer Session ändern - zum Beispiel ein Entitlement-Flag, das aktualisiert wird, wenn ein Nutzer seinen Plan upgradet, ohne einen vollständigen Seitenreload. Für alles andere ist die serverseitige Auswertung einfacher und zuverlässiger.

Die Flag-Abruffunktion kann deinen eigenen API-Endpunkt aufrufen, der zum selben Symfony-Backend proxyt - eine einzige Quelle der Wahrheit statt zwei separater Flag-Stores.

// lib/flags.ts
export async function getFeatureFlag(name: string): Promise<boolean> {
  const res = await fetch(`${process.env.API_BASE_URL}/feature-flags/${name}`, {
    next: { revalidate: 60 }, // ISR-artiger Cache
  });
  if (!res.ok) return false; // fail closed
  const data = await res.json();
  return data.enabled ?? false;
}

Progressiver Rollout ohne LaunchDarkly

LaunchDarkly ist ausgezeichnete Software, aber seine Unternehmenspreise - oft 1.000-3.000 Euro pro Monat für ein kleines Team - sind schwer zu rechtfertigen, wenn der Großteil des Mehrwerts aus prozentualen Rollouts und Targeting-Regeln kommt, die du selbst in wenigen Stunden implementieren kannst.

Für prozentuale Rollouts liefert ein deterministischer Hash aus Nutzer-ID und Flag-Name konsistente Zuordnungen, ohne pro-Nutzer-Flag-Zustand zu speichern:

public function isEnabledForUser(string $flagName, string $userId): bool
{
    $flag = $this->repository->findOneByName($flagName);
    if (null === $flag || !$flag->isEnabled()) {
        return false;
    }

    $rolloutPercentage = $flag->getRolloutPercentage() ?? 100;
    if ($rolloutPercentage >= 100) {
        return true;
    }

    // Deterministisch: gleicher Nutzer erhält immer gleiches Ergebnis für gleiches Flag
    $hash = crc32($flagName . ':' . $userId) % 100;
    return $hash < $rolloutPercentage;
}

Füge eine rollout_percentage-Integer-Spalte (0-100, nullable, Standard null bedeutet 100%) zur FeatureFlag-Entity hinzu und du hast prozentuale Rollouts. Um die Exposition schrittweise zu erhöhen, aktualisiere den Spaltenwert in deinem Admin-Panel - kein Redeployment nötig.

Für Targeting-Regeln jenseits von Prozentsätzen (nach Organisation, Plan oder geografischer Region) speichert eine JSON-context-Spalte im Flag die Targeting-Logik, und der Service wertet sie gegen ein Kontext-Array aus, das bei der Aufrufzeit übergeben wird. Das deckt die überwiegende Mehrheit dessen ab, was Teams von einem kommerziellen Flag-Service tatsächlich benötigen.

Open-Source-Alternativen, die es wert sind, evaluiert zu werden, wenn du einen dedizierten Service statt einer eigenen Implementierung möchtest: Unleash (selbst hostbar, starkes Symfony-SDK), Flipt (einzelnes Binary, gRPC und REST) und Flagsmith (großzügiger kostenloser Tarif für Hosted, selbst hostbar für Enterprise). Jedes davon läuft problemlos auf einem kleinen VPS neben einem SaaS-Stack.

Feature Flags mit deiner Deployment-Pipeline verbinden

Das letzte Element, das Progressive Delivery nahtlos anfühlen lässt, ist das Schließen der Schleife zwischen CI/CD und dem Flag-Store. Zwei Integrationen machen den größten Unterschied.

Erstens: Führe einen Flag-Audit-Schritt in deiner CI-Pipeline aus. Ein einfaches Skript liest alle isEnabled(...)-Aufrufe in der Codebasis, gleicht sie mit dem Flag-Store ab und lässt den Build fehlschlagen, wenn ein Flag im Code nicht im Store existiert (ein Tippfehler-Risiko) oder wenn ein Flag im Store seit mehr als 90 Tagen nicht im Code referenziert wurde (ein Friedhof-Kandidat).

Zweitens: Deployment-Events verdrahten. Wenn ein Deployment erfolgreich ist, schalte das Ziel-Release-Flag automatisch für ein internes Nutzersegment um (z.B. Nutzer mit einer @yourcompany.com-E-Mail). Nach 24 Stunden ohne Fehler erhöht ein Scheduled-Task den Rollout-Prozentsatz auf 10%, dann 50%, dann 100% in einem konfigurierbaren Rhythmus. Das ist echte Progressive Delivery - die Art, die Rollbacks zu einem letzten Ausweg macht statt zu einem wöchentlichen Vorgang.

Ein Hinweis zum Monitoring

Feature Flags sind nur so nützlich wie deine Observability rund um sie. Bevor du ein Release-Flag über interne Nutzer hinaus aktivierst, stelle sicher, dass dein Error-Tracking (Sentry, Bugsnag) nach Flag-Zustand filtern kann und dass deine Schlüsselmetriken (Fehlerrate, p95-Antwortzeit, Konversionsrate) pro Flag-Kohorte protokolliert werden. Ohne das kannst du nicht erkennen, ob ein Anstieg nach einem Rollout durch den neuen Code oder durch ein unabhängiges Deployment verursacht wird.

Das ist die Disziplinlücke zwischen Teams, die Feature Flags erfolgreich einsetzen, und Teams, die sie ein Jahr lang nutzen und dann frustriert wieder herausreißen.

Es von Anfang an richtig machen

Feature Flags sind Infrastruktur, kein Plugin, das man hinzufügt, wenn man groß genug ist. Eine saubere Implementierung von Anfang an - typisierte Flags, expliziter Lebenszyklus, Eigentümerschaft, prozentuale Rollout-Unterstützung - kostet vielleicht zwei Tage Engineering-Zeit und zahlt sich beim ersten Mal zurück, wenn du ein Produktionsproblem abfängst, bevor es alle deine Nutzer erreicht.

Wenn deine Codebasis bereits Flags ohne Struktur verteilt hat, kann ein systematisches Audit sie von einer Verbindlichkeit in ein Asset verwandeln. Das beinhaltet das Katalogisieren jedes Flags, Klassifizieren nach Typ, Zuweisen von Eigentümerschaft und Aufbauen des Toolings, um künftige Friedhof-Ansammlungen zu verhindern.

Wenn du das gerade aufbaust und eine zweite Meinung zur Architektur möchtest - oder wenn du einen Flag-Friedhof geerbt hast und Hilfe beim Aufräumen brauchst - melde dich unter hello@wolf-tech.io oder schau dir den Code-Quality-Consulting-Service bei wolf-tech.io an. Strukturelle Probleme wie dieses sind genau das, wofür dieser Service konzipiert ist.


FAQ

Brauche ich einen Feature-Flag-Service wie LaunchDarkly, um Feature Flags effektiv zu nutzen?

Nein. Für die meisten SaaS-Teams deckt eine datenbankbasierte Implementierung mit prozentualer Rollout-Unterstützung den vollständigen Feature-Satz ab. Kommerzielle Services fügen bei Skalierung oder wenn du Echtzeit-Analysen und A/B-Testing-Integrationen benötigst einen Mehrwert hinzu, sind aber für den Start nicht erforderlich.

Was ist der Unterschied zwischen einem Feature Flag und einer Umgebungsvariablen?

Umgebungsvariablen werden zur Deploy-Zeit gesetzt und erfordern ein Redeployment zum Ändern. Feature Flags können zur Laufzeit umgeschaltet werden, ohne die Deployment-Pipeline anzufassen. Verwende Umgebungsvariablen für Infrastrukturkonfiguration; verwende Feature Flags für Code-Verhalten, das du möglicherweise ändern musst, während das System läuft.

Wie verhindere ich Friedhof-Ansammlungen?

Weise jedem Release-Flag bei der Erstellung ein angestrebtes Entfernungsdatum zu. Führe einen CI-Check aus, der warnt oder fehlschlägt, wenn Flags ihr Ablaufdatum überschreiten. Mache Flag-Cleanup zu einer erstklassigen Engineering-Aufgabe - nicht zu etwas, das auf "nächstes Quartal" verschoben wird.

Können Feature Flags meine Anwendung verlangsamen?

Mit Caching ist der Overhead vernachlässigbar - ein einzelner Cache-Lookup pro Anfrage. Ohne Caching können datenbankbasierte Flags Latenz hinzufügen, wenn sie häufig in Hot-Paths überprüft werden. Halte Flag-Checks aus engen Schleifen heraus, cache aggressiv und überwache die Query-Anzahl, wenn du Flags zu Traffic-intensiven Endpunkten hinzufügst.