Ein Modernisierungsschritt, der nur ganz oder gar nicht ausgerollt werden kann, ist ein Risiko, das mit der Größe des Schritts wächst. Deshalb wird bei jeder ernsthaften schrittweisen Modernisierung irgendwann derselbe Wunsch laut: Wir wollen den neuen Weg einschalten können, für einen Kunden, für ein Prozent, für uns selbst, und ihn in Sekunden wieder abschalten.
Das Mittel dafür heißt Feature Flag, und in einem modernen Framework ist es ein Paket und eine halbe Stunde. In einem gewachsenen Monolithen ohne Unterstützung dafür wirkt es wie ein eigenes Projekt, und genau deshalb wird es oft weggelassen.
Das muss es nicht sein. Dieser Artikel beschreibt eine Umsetzung, die an einem Tag steht, in jeder PHP-Anwendung funktioniert und den Teil ernst nimmt, an dem Flags tatsächlich scheitern: das Wiederentfernen.
Wofür Flags in einem Altsystem gut sind
Die Beispiele in der Literatur handeln von Produktversuchen: zwei Farben für einen Knopf, gemessen an der Abschlussrate. Darum geht es hier nicht, und wer mit dieser Erwartung anfängt, baut zu viel.
In einer Modernisierung haben Flags drei Aufgaben, und alle drei sind langweilig.
Der alte und der neue Weg laufen nebeneinander. Die neue Preisberechnung ist fertig, sie ist getestet, und trotzdem will niemand sie am Montag für alle einschalten. Mit einem Schalter läuft sie zuerst für interne Nutzer, dann für einen Kunden, dann für alle. Jeder dieser Schritte ist eine Entscheidung, kein Deployment.
Ein Rückweg, der keine Auslieferung braucht. Wenn der neue Weg um 14 Uhr Fehler wirft, ist der Schalter in einer Minute umgelegt. Eine Rückrolle des gesamten Releases dauert länger und nimmt alles andere mit, was seitdem ausgeliefert wurde.
Unfertiges kann ausgeliefert werden. Das ist der unterschätzte Punkt. Ohne Flags sammelt sich Arbeit in einem langlebigen Zweig, und beim Zusammenführen liegen Wochen an Änderungen aufeinander. Mit Flags geht der halbfertige neue Weg jede Woche mit, abgeschaltet, und wird nie zu einem großen Zusammenführen.
Ein Vergleich, der das einordnet: Der Schalter ist das, was einen Umbau von einer Umstellung unterscheidet. Ein Neubau hat keinen Schalter, und das ist einer der Gründe, warum er so oft scheitert.
Vier Arten von Flags, drei mit Verfallsdatum
Flags werden dann zum Problem, wenn sie alle gleich behandelt werden. Vier Arten, und die Unterscheidung entscheidet, wie lange ein Flag leben darf.
Umstellungsflags begleiten einen Umbau und verschwinden mit ihm. Lebensdauer: Tage bis wenige Wochen. Das sind neunzig Prozent der Flags in einer Modernisierung.
Betriebsflags schalten etwas ab, wenn es brennt: einen teuren Bericht, eine Schnittstelle zu einem Partner, der gerade nicht antwortet. Sie leben dauerhaft, und das ist in Ordnung, weil sie eine Betriebsfunktion sind.
Berechtigungsflags schalten Funktionen je Kunde oder Tarif frei. Die leben ebenfalls dauerhaft, gehören aber nicht in dasselbe System: Das ist Fachlogik und keine Umstellung, und sie gehört ins Datenmodell, nicht in eine Schaltertabelle.
Vergessene Flags sind keine eigene Art, sondern das Ergebnis. Ein Flag aus einem Umbau von 2023, das noch abgefragt wird, dessen beide Zweige noch existieren und von dem niemand weiß, welcher richtig ist. Genau davor schützt der Abschnitt über das Aufräumen.
Die einfachste Umsetzung, die trägt
Was es braucht, ist eine Tabelle, eine Klasse und ein Zwischenspeicher. Kein Dienst, kein Paket, keine Oberfläche.
CREATE TABLE feature_flag (
name VARCHAR(100) NOT NULL PRIMARY KEY,
aktiv TINYINT(1) NOT NULL DEFAULT 0,
-- Prozentsatz statt nur an/aus: der schrittweise Weg ist der Normalfall
anteil TINYINT NOT NULL DEFAULT 0,
beschreibung VARCHAR(255) NOT NULL,
-- Das wichtigste Feld der Tabelle, siehe Abschnitt zum Aufraeumen
entfernen_bis DATE NULL,
geaendert_am DATETIME NOT NULL
) ENGINE=InnoDB;Die Abfrage dazu muss zwei Bedingungen erfüllen: Sie darf die Anfrage nicht messbar verlangsamen, und sie muss für denselben Nutzer stabil antworten. Das Zweite ist der Punkt, an dem selbstgebaute Lösungen meistens scheitern.
final class Schalter
{
private array $flags;
public function __construct(private PDO $db, private CacheInterface $cache) {}
public function an(string $name, ?string $schluessel = null): bool
{
// Einmal je Anfrage laden, nicht einmal je Aufruf.
$this->flags ??= $this->cache->get('feature_flags', fn () =>
$this->db->query('SELECT name, aktiv, anteil FROM feature_flag')
->fetchAll(PDO::FETCH_UNIQUE), 30);
$f = $this->flags[$name] ?? null;
if ($f === null) return false; // Unbekanntes Flag ist aus, nie an.
if (!$f['aktiv']) return false;
if ($f['anteil'] >= 100) return true;
if ($schluessel === null) return false;
// Stabil: derselbe Nutzer bekommt bei jedem Aufruf dieselbe Antwort.
// Mit rand() waere die Haelfte der Anfragen eines Nutzers auf dem
// alten und die andere auf dem neuen Weg, und jeder Fehlerbericht
// waere wertlos. Der Flagname geht in den Hash ein, damit nicht
// immer dieselben Nutzer in jeder Testgruppe landen.
$wert = crc32($name . ':' . $schluessel) % 100;
return $wert < $f['anteil'];
}
}Drei Entscheidungen darin sind wichtiger als der Rest.
Unbekannt heißt aus. Wenn die Tabelle nicht erreichbar ist oder das Flag fehlt, läuft der alte Weg. Ein Flag, dessen Ausfall den neuen Weg einschaltet, ist ein Schalter, der im Notfall in die falsche Richtung fällt.
Der Zwischenspeicher ist kurz und bewusst. Dreißig Sekunden heißt: Ein Abschalten wirkt spätestens nach dreißig Sekunden. Das ist der Preis dafür, dass nicht jede Anfrage die Datenbank fragt, und dreißig Sekunden sind im Notfall vertretbar. Ohne jeden Zwischenspeicher wird die Tabelle zum meistgelesenen Objekt im System.
Der Anteil wird gehasht, nicht gewürfelt. Siehe Kommentar im Code. Das ist der Fehler, den ich am häufigsten in selbstgebauten Umsetzungen finde.
Wo der Schalter steht: so weit außen wie möglich
Die Frage, die über die Aufräumbarkeit entscheidet, ist nicht, wie das Flag funktioniert, sondern wo es abgefragt wird.
Falsch ist die Abfrage tief in der Logik, verteilt über viele Stellen:
// Schlecht: das Flag sitzt mitten in der Methode, und es sitzt dort
// noch an fuenf weiteren Stellen.
public function berechne(Warenkorb $k): Betrag
{
if ($this->schalter->an('neue_rabatte', $k->kundeId())) {
$rabatt = $this->neuerRabatt($k);
} else {
$rabatt = $this->alterRabatt($k);
}
// ... und weiter unten nochmal
}Richtig ist eine Abfrage an der Kante, die zwei vollständige Umsetzungen auseinanderhält:
// Gut: eine Stelle entscheidet, welche Umsetzung benutzt wird.
// Beide Klassen erfuellen dieselbe Schnittstelle, keine kennt das Flag.
$rechner = $this->schalter->an('neue_rabatte', $kunde->id())
? new RabattRechnerNeu($this->staffeln)
: new RabattRechnerAlt($this->db);
return $rechner->berechne($warenkorb);Der Unterschied zeigt sich beim Aufräumen. In der ersten Fassung muss jemand fünf Stellen finden und in jeder den richtigen Zweig behalten. In der zweiten wird eine Zeile ersetzt und eine Klasse gelöscht, und der Rest des Codes weiß nicht einmal, dass es je ein Flag gab.
Wo eine Schnittstelle noch nicht existiert, ist genau das der erste Arbeitsschritt: eine schmale Schicht, hinter der beide Wege liegen können. Das ist derselbe Gedanke wie beim Anti-Corruption Layer, nur nach innen gerichtet.
Flags und die Datenbank
Ein Schalter dreht Code um. Daten dreht er nicht um, und daraus folgt die einzige Regel, die wirklich zwingend ist: Beide Zweige müssen mit denselben Daten leben können.
Praktisch heißt das dreierlei. Neue Spalten werden hinzugefügt, nicht umgewidmet. Der alte Zweig ignoriert, was er nicht kennt. Und der neue Zweig schreibt nichts, was den alten Zweig beim Zurückschalten stolpern lässt.
Wo das nicht geht, weil die neue Logik andere Daten erzeugt, gibt es zwei ehrliche Auswege. Entweder der neue Zweig schreibt zunächst in eigene Spalten und nur der Lesepfad wird geschaltet. Oder der Schalter bekommt eine Einbahnstraße: ab einem bestimmten Zeitpunkt ist Zurückschalten nicht mehr möglich, und das steht als Kommentar im Code und als Datum an der Tabelle.
Die schlechteste Lösung ist die, bei der niemand darüber nachgedacht hat. Dann wird um 14 Uhr zurückgeschaltet, und um 15 Uhr stellt sich heraus, dass die seitdem angelegten Vorgänge im alten Zweig unsichtbar sind.
Ein Schalter ist auch das Werkzeug, um etwas abzuschalten, das niemand mehr braucht: Funktionen entfernen, die niemand mehr nutzt.
Das Aufräumen ist der eigentliche Teil
Jedes Flag ist eine Verzweigung. Zwanzig Flags sind eine Million theoretischer Zustände, von denen niemand mehr weiß, welche getestet sind. Ein Altsystem, das man gerade einfacher machen wollte, wird dadurch komplizierter, und das ist der Grund, warum Flags in vielen Häusern einen schlechten Ruf haben.
Was dagegen hilft, sind drei Dinge, und keines davon ist Disziplin.
Das Feld entfernen_bis ist Pflicht. Beim Anlegen eines Umstellungsflags wird ein Datum gesetzt, meistens sechs bis acht Wochen. Ohne Datum wird das Flag nicht angelegt, und Betriebsflags tragen bewusst NULL.
Ein wöchentlicher Bericht nennt die Überfälligen, mit Namen des Teams. Das ist ein Skript und eine Nachricht in den Chat, keine Sitzung:
SELECT name, beschreibung, entfernen_bis,
DATEDIFF(CURDATE(), entfernen_bis) AS tage_ueberfaellig
FROM feature_flag
WHERE entfernen_bis IS NOT NULL AND entfernen_bis < CURDATE()
ORDER BY entfernen_bis;Das Entfernen ist Teil der Aufgabe, nicht eine neue. Eine Umstellung gilt erst als fertig, wenn das Flag und der alte Zweig weg sind. Solange das als eigenes, späteres Ticket geführt wird, wird es nicht gemacht, weil es nie dringend ist.
Als Anhaltspunkt aus der Praxis: Mehr als fünf bis acht gleichzeitig offene Umstellungsflags sind in einem Team ein Zeichen dafür, dass zu viele Umbauten gleichzeitig laufen, nicht dafür, dass Flags gut funktionieren.
Für das stufenweise Ausrollen einer neuen Fassung gibt es den Weg über den Lastverteiler statt über den Schalter: Blue/Green und Canary ohne Kubernetes.
Wann Sie keine Flags brauchen
Drei Fälle, in denen der Schalter mehr kostet, als er bringt.
Die Änderung ist klein und rückwärtskompatibel. Für eine korrigierte Berechnung, die dasselbe Ergebnis besser erzeugt, ist ein Flag Zeremonie. Ausrollen, messen, fertig.
Es gibt keinen Weg, den Erfolg zu sehen. Ein Flag ohne Beobachtung ist ein Schalter im Dunkeln. Wenn niemand messen kann, ob der neue Weg besser läuft, ist die Reihenfolge falsch: erst die Messung, dann die Umstellung.
Die Auslieferung ist ohnehin ein Knopfdruck und in einer Minute zurückgedreht. Dann ist der Rückweg schon da, und ein Flag lohnt nur noch für die Fälle, in denen etwas schrittweise eingeschaltet werden soll. Wie man zu einem solchen Rollout kommt, steht auf einer eigenen Seite, und die Reihenfolge ist kein Zufall: Erst der Rollout, dann die Flags.
Wie eine schrittweise Modernisierung insgesamt geschnitten wird, damit solche Schalter überhaupt nötig und nützlich sind, steht auf der Seite zur Legacy-Modernisierung.
Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.

