This article is also available in English

In der Regel schreiben wir Software nicht zum reinen Selbstzweck, sondern um irgendeine Art von Fachlichkeit zu unterstützen und somit einen Mehrwert zu stiften. Dieser Mehrwert kommt allerdings erst dann zur Entfaltung, wenn die Software auch genutzt, also aktiv verwendet, wird. In Zeiten von zentralen Web- und Software-as-a-Service-Anwendungen bedeutet das in vielen Projekten, dass wir auf Continuous Delivery setzen. Ziel dabei ist es, die Zeit von Idee bis zur Nutzung in Produktion zu minimieren und idealerweise jede Änderung sofort in Produktion zu nehmen.

Neben vielen anderen Fragen stellt sich hier auch die Frage, wie wir mit neuen Features umgehen wollen. Wie nehmen wir ein Feature ab, bevor es für alle sichtbar ist? Wie koordinieren wir Features über mehrere Systeme hinweg? Welche Auswirkungen auf Performanz und Stabilität hat das neue Feature? Ist das neue Feature wirklich fehlerfrei?

Viele dieser Fragen lassen sich dadurch lösen, dass wir vor einem Produktionsdeployment einen optimalen Qualitätsprozess aufbauen. Dieser verlängert allerdings wiederum die Zeit, bis der neue Code einen Mehrwert stiften kann. Eine Alternative hierzu kann das Nutzen von Feature Flags, auch Feature Toggles oder Eigenschaftenschalter genannt, sein. Trotzdem können und sollten wir auch bei deren Einsatz nicht auf einen guten Qualitätsprozess verzichten. Vielmehr ergänzen sich beide Ansätze. Feature Flags reduzieren das Risiko während eines Deployments und haben, wie wir später auch noch sehen werden, noch weitere Vorteile.

Im Folgenden wollen wir uns im Detail anschauen, was ein Feature Flag überhaupt genau ist, welche Ansätze wir mit einem solchen verfolgen können und wie dies in Java mit den drei Bibliotheken FF4j, Togglz oder Unleash umgesetzt werden kann.

Was ist überhaupt ein Feature Flag?

Im Kern handelt es sich bei einem Feature Flag um einen Wahrheitswert, den wir im Code abfragen und anschließend darauf reagieren können. Angenommen, unser neues Feature soll unter einem Artikel noch eine Liste von Empfehlungen zu anderen Artikeln anzeigen. Das Feature ist schon fertig entwickelt, aber soll noch nicht aktiviert werden, weil wir noch auf eine Marketingkampagne warten müssen. Wir haben nun zwei Möglichkeiten. Entweder wir lassen das Feature auf einem Branch, der nicht deployt wird, in der Versionskontrolle, warten bis zur Kampagne und mergen und deployen den Code im passenden Moment oder wir sichern das Feature mit einem Feature Flag ab. Die Liste wird nun also nur angezeigt, falls das Feature Flag aktiviert ist.

Aber auch für größere oder gefährliche Refactorings können Feature Flags genutzt werden. Hierzu fragen wir an der passenden Stelle das Feature Flag ab und rufen je nach Zustand den alten oder den neuen Code auf. Theoretisch können wir hier auch beide Codepfade aufrufen und das Ergebnis miteinander vergleichen. Bei einem Refactoring sollten ja beide identisch sein.

Bis jetzt könnten wir auf die Idee kommen, Feature Flags selber abzubilden und an den passenden Stellen im Code mit einer Booleschen Variable zu arbeiten. Dies würde allerdings dazu führen, dass wir für eine Änderung des Feature Flags auch eine Codeänderung und ein neues Deployment benötigen. Das ist zwar möglich, würde aber eine ganze Klasse von Einsatzmöglichkeiten ausschließen.

Der nächste Schritt ist es also, das Feature Flag konfigurierbar zu machen. Somit kann beim Start der Anwendung entschieden werden, welche Features aktiv sind. Mit diesem Mechanismus kann ich in einer Test-Umgebung ein neues Feature aktivieren, damit es anschließend abgenommen wird, und gleichzeitig nach Produktion deployen. Da das Feature dort noch nicht aktiv ist, schlummert der Code dort vor sich hin, ohne aufgerufen zu werden.

Feature Flag-Bibliotheken gehen allerdings noch einen Schritt weiter. Mit diesen kann ich den Zustand eines Feature Flags zur Laufzeit mit diversen Strategien bestimmen. Hierzu wird bei der Abfrage eines Feature Flags zusätzlich ein Feature Context, mit beispielsweise den Informationen über den Nutzenden, ausgewertet. Somit kann der Zustand des Feature Flags individuell für den konkreten Durchlauf ermittelt werden.

Jetzt, wo wir das Prinzip von Feature Flags kennengelernt haben, wollen wir uns im nächsten Schritt konkrete Einsatzmöglichkeiten anschauen.

Einsatzmöglichkeiten für Feature Flags

Für Feature Flags gibt es, wie bereits angedeutet, diverse Einsatzmöglichkeiten. Diese lassen sich grob in die vier Kategorien einteilen:

Die Kategorien entscheiden sich dabei nicht in der konkreten Verwendung im Code, sondern primär durch das Ziel, das wir damit erreichen wollen. Durch das Ziel wiederum ergibt sich für jede Kategorie eine unterschiedliche Anforderung daran, wie dynamisch wir den Zustand des Feature Flags ermitteln müssen und wie lange ein solches Feature Flag sich in unserer Codebasis befindet.

Bei Release Feature Flags geht es primär darum, das Release eines neuen Features vom Zeitpunkt des Deployments zu entkoppeln. Zum einen ermöglicht uns dieses Vorgehen, auch Code zu deployen, der noch nicht zu 100 Prozent fertig entwickelt ist. Dieser wird, da durch ein Feature Flag geschützt, nie ausgeführt. Andererseits lassen sich so auch Features bereits mitdeployen, die aus anderen Gründen zum jetzigen Zeitpunkt noch nicht nutzbar sein sollen, aber bereits fertig sind.

Beides kann, wie bereits gesagt, als Alternative zu Branches im Versionskontrollsystem angesehen werden. Der Vorteil von Feature Flags besteht darin, dass die Integration von neuem Code bereits geschehen ist. Uns erwarten also, durch langlaufende Branches, keine Merge-Konflikte mehr.

Diese Art von Feature Flags befinden sich in der Regel nur sehr kurzfristig in unserer Codebasis. Sobald die Features aktiviert wurden, können und sollten wir die Feature Flags entfernen. Auch brauchen wir hier keine hohe Dynamik. In der Regel können wir zur Aktivierung eines solchen Feature Flags sogar ein neues Deployment nutzen, brauchen als nicht zwangsweise eine Konfigurationsmöglichkeit zur Laufzeit.

Auch für den Betrieb können Feature Flags hilfreich sein. Beispielsweise, indem sich Teile der Anwendung zur Laufzeit ausschalten lassen, um Performanz oder Stabilität zu gewährleisten. Neben einem expliziten Kill Switch können wir dieses Prinzip auch für den graduellen Roll-out eines neuen Features einsetzen. Hierzu aktivieren wir das neue Feature nur für eine gewisse prozentuale Anzahl von Anfragen oder Nutzenden und beobachten deren Auswirkungen. Wenn die Anwendung weiterhin stabil läuft, können wir diese Anzahl so lange anheben, bis letztlich alle das Feature nutzen können. Dies lässt sich natürlich auch durch weitere Infrastruktur, beispielsweise im Load-Balancer, lösen.

Als dritte Möglichkeit steht dann noch die Nutzung von Feature Flags für einen Wartungsmodus zur Verfügung. Dies hilft vor allem, wenn ein System, mit dem wir interagieren müssen, für einen gewissen Zeitraum nicht zur Verfügung steht. Wir können dann unsere Anwendung, oder zumindest die Stellen, die das andere System aufrufen, in den Wartungsmodus versetzen und somit fehlerhafte Aufrufe, und damit auch Log-Nachrichten, die gegebenenfalls Alarme produzieren, vermeiden.

Die Verweilzeit dieser Feature Flags im Code variiert von kurz- bis langfristig. Feature Flags für den graduellen Roll-out werden direkt nach erfolgtem Deployment entfernt. Kill Switches oder der Wartungsmodus bleibt dahingegen vermutlich für immer im Code. Von der Dynamik her befinden sich solche Features Flags in der Mitte des Spektrums. Sie werden in der Regel nicht erst statisch über ein Deployment geändert, müssen aber auch nicht alle bei jedem Request ausgewertet werden.

Berechtigungen bilden wir in der Regel über ein eigenes Konzept ab. Es kann allerdings sinnvoll sein, an dieser Stelle Feature Flags zu nutzen und deren Zustand anhand einer Berechtigung zu prüfen. So ist es uns anschließend beispielsweise möglich, ein Feature für ein bestimmtes Land zu aktiveren und hier einen Early Access zu ermöglichen. Auch weitere Dinge, wie das Blockieren von Nutzenden, beispielsweise aufgrund lokaler Gesetze, das Ermöglichen, neue Features als erster per Opt-in zu testen, oder Nutzenden je nach Erfahrung erweiterte Oberflächen anzubieten, sind möglich.

Diese Feature Flags benötigen eine hohe Dynamik, da für die Auswertung des Zustands immer Informationen über den aktuellen Nutzenden benötigt werden. Zudem bleiben diese auch sehr lange in unserer Codebasis, nämlich mindestens so lange, wie es das Feature, das sie absichern, gibt.

Auch für Experimente innerhalb unserer Anwendung bieten sich Feature Flags an. Gerade für A/B-Tests sind sie enorm hilfreich. Hierzu werden die Nutzenden unserer Anwendung in zwei Gruppen geteilt. Die eine Gruppe sieht die Variante A, die andere B. Erheben wir nun, welche der beiden Gruppen das Ziel, das wir verfolgen, besser unterstützt, wissen wir, welche Variante wir in Zukunft beibehalten sollten.

Solche Feature Flags sind in der Regel nur kurz bis mittelfristig im Code vorhanden, nämlich solange das Experiment läuft. Dafür benötigen sie allerdings eine hohe Dynamik, da jeder eingehende Request zu einem anderen Nutzenden gehören kann und somit der Codepfad ein anderer sein kann.

Nachdem wir nun also wissen, was Feature Flags sind und für welche Anwendungsfälle diese verwendet werden können, wollen wir uns nun anschauen, wie wir diese in Java mithilfe von Bibliotheken nutzen können. Wir starten dabei mit FF4j.

FF4j

FF4j gibt es nun bereits seit über fünf Jahren und bietet uns vor allem eine große Auswahl, wenn es um die Persistenz der Feature Flags geht. Doch bevor wir hierzu kommen, sollten wir uns erst einmal die eigentliche Programmierschnittstelle anschauen.

Den Haupteinstiegspunkt in FF4j bildet die gleichnamige Klasse FF4j. Innerhalb unserer Anwendung benötigen wir eine Instanz von dieser, um mithilfe der Methode check prüfen zu können, ob ein Feature Flag aktiv ist oder nicht. In Listing 1 ist zu sehen, wie wir eine solche Instanz erzeugen, zwei Feature Flags registrieren und anschließend deren Status abfragen.

package de.mvitz.featureflags.ff4j;

import org.ff4j.FF4j;

public class FF4jExample {

    public static void main(String[] args) {
        FF4j ff4j = new FF4j();
        ff4j.createFeature("mein-feature-a", true);
        ff4j.createFeature("mein-feature-b", false);

        System.out.println(ff4j.check("mein-feature-a"));
        System.out.println(ff4j.check("mein-feature-b"));
    }
}
Listing 1: Konfiguration und Nutzung von FF4j

Wenn ein Feature Flag aktiv ist, prüft FF4j in einem zweiten Schritt, ob der aktuelle Benutzer auch die benötigten Berechtigungen hat. Das Ermitteln der Berechtigungen selber delegiert FF4j an eine Implementierung von AuthorizationsManager. Diese Implementierung kann dann beispielsweise auf Spring Security zurückgreifen, um die Rollen des Nutzers zu ermitteln.

Im dritten Schritt prüft FF4j dann zusätzlich mittels einer FlippingStrategy, ob das aktivierte Feature für genau diesen Aufruf wirk- lich aktiv ist. So ist es möglich, Strategien wie einen graduellen Roll-out oder einen datumsbasierten Launch umzusetzen, ohne eitere Infrastruktur zu benötigen. Listing 2 zeigt, wie wir ein Feature programmatisch anlegen, das erst ab dem 24.12.2020 um 12 Uhr aktiv ist.

package de.mvitz.featureflags.ff4j;

import org.ff4j.FF4j;
import org.ff4j.core.Feature;
import org.ff4j.core.FlippingStrategy;
import org.ff4j.strategy.time.ReleaseDateFlipStrategy;

public class FF4jExampleFlippingStrategy {

    public static void main(String[] args) {
        final FlippingStrategy strategy =
            new ReleaseDateFlipStrategy("2020-12-24-12:00");

        final Feature feature = new Feature("mein-feature");
        feature.setEnable(true);
        feature.setFlippingStrategy(strategy);

        final FF4j ff4j = new FF4j();
        ff4j.createFeature(feature);

        System.out.println(ff4j.check("mein-feature"));
    }
}
Listing 2: Datumsbasierter Launch mittels FF4j

Neben dieser Strategy bringt FF4j noch einige weitere mit und durch eine eigene Implementierung von FlippingStrategy lassen sich auch noch nicht vorhandene Strategien umsetzen.

Um den Zustand unserer Feature Flags auch zwischen mehreren Anwendungen beziehungsweise Instanzen unserer Anwendung zu teilen, ermöglicht es uns FF4j, selber zu wählen, wie dieser persistiert werden soll. FF4j bietet uns hierzu bereits mehr als zehn Implementierungen des FeatureStore-Interfaces für diverse verbreitete Datenbanken. Zudem gibt es auch eine Weboberfläche, um den Zustand unserer Feature Flags zur Laufzeit einzusehen und zu ändern.

Togglz

Obwohl Togglz konzeptionell weitestgehend identisch zu FF4j ist, unterscheiden sich die APIs zur Abfrage der Feature Flags deutlich. Togglz setzt hier auf Java Enums als primäres Mittel. In Listing 3 sehen wir hierbei das Pendant zur FF4j-Variante aus Listing 1.

package de.mvitz.featureflags.togglz;

import org.togglz.core.Feature;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.context.FeatureContext;
import org.togglz.core.context.StaticFeatureManagerProvider;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.manager.FeatureManagerBuilder;

public class TogglzExample {

    public enum MyFeatures implements Feature {
        @EnabledByDefault A,
        B;

        public boolean isActive() {
            return FeatureContext.getFeatureManager()
                .isActive(this);
        }
    }

    public static void main(String[] args) {
        final FeatureManager featureManager = FeatureManagerBuilder
            .begin()
            .featureEnum(MyFeatures.class)
            .build();
        StaticFeatureManagerProvider.setFeatureManager(featureManager);

        System.out.println(MyFeatures.A.isActive());
        System.out.println(MyFeatures.B.isActive());
    }
}
Listing 3: Konfiguration und Nutzung von Togglz

Durch die Nutzung von Enums für die Feature Flag-Prüfungen ergeben sich bei der Nutzung leichte Unterschiede. Zum einen brauchen wir an den Stellen, an denen der Check durchgeführt werden soll, keinen Zugriff mehr auf eine konkrete Instanz einer Klasse, sondern können das Enum direkt, sozusagen statisch, verwenden. Zum anderen ist die Programmierschnittstelle so ein klein wenig typisierter und es kann keine Abfrage mehr für ein nicht vorhandenes Feature geben.

Abseits davon ist der Aufbau relativ gleich zu FF4j, nur mit anderen Namen. Für die Prüfung von Berechtigungen wird das Interface UserProvider implementiert, und für Strategien wird ActivationStrategy genutzt. Wie auch FF4j bringt Togglz von Haus aus ein paar Strategien mit und auch die Persistenz ist konfigurierbar. Zudem bringt auch Togglz eine Weboberfläche zur Administration mit.

Unleash

Neben FF4j und Togglz gibt es mit Unleash noch einen dritten Vertreter, den ich hier zeigen möchte. Von der Programmierschnittstelle her ist Unleash nahezu identisch mit der vom FF4j. Es gibt das Interface Unleash, über das wir per isEnabled-Methode den Zustand eines Flags abfragen können (s. Listing 4).

package de.mvitz.featureflags.unleash;

import no.finn.unleash.DefaultUnleash;
import no.finn.unleash.Unleash;
import no.finn.unleash.util.UnleashConfig;

public class UnleashExample {

    public static void main(String[] args) {
        UnleashConfig config = UnleashConfig.builder()
            .appName("unleash-example")
            .unleashAPI("http://....")
            .build();
        Unleash unleash = new DefaultUnleash(config);
        System.out.println(unleash.isEnabled("mein-feature-a"));
    }
}
Listing 4: Konfiguration und Nutzung von Unleash

Unleash bietet im Vergleich zu den beiden vorher genannten Bibliotheken jedoch drei Vorteile. Der erste Vorteil besteht darin, dass Unleash nicht als Feature Flag-Bibliothek geschrieben wurde, sondern hier die Persistenz und das Admin-Interface das Herzstück darstellen. Hierzu wird Unleash als Webanwendung betrieben, die eine Programmierschnittstelle zur Verfügung stellt, mit der die Bibliothek dann kommuniziert. Unleash selber ist Open Source und kann demnach auch selber ohne Kosten gehostet werden. Es gibt jedoch auch eine Enterprise-Version mit zusätzlichen Features und auch eine Software-as-a-Service-Variante.

Dadurch, dass Unleash eben als Service konzipiert ist, ergibt sich der zweite Vorteil. Neben dem Unleash-Client für Java gibt es für nahezu alle aktuell relevanten Programmiersprachen fertige Clients. So können wir Feature Flags über mehrere Anwendungen teilen beziehungsweise brauchen nur eine zentrale Oberfläche zur Administration der Feature Flags.

Der dritte Vorteil besteht darin, dass GitLab mittlerweile auch ein Feature Flag-Feature mitbringt. Dabei lassen sich unsere Feature Flags in GitLab verwalten und anschließend aus der Anwendung über das Unleash-API abfragen. Somit würde in diesem Szenario sogar das Hosten des Unleash-Services entfallen.

Auch wenn Unleash meiner Erfahrung nach nicht so verbreitet ist wie FF4j oder Togglz, bietet es sich an, bei einer Auswahl mindestens diese drei Bibliotheken anzuschauen.

Fazit

In diesem Artikel haben wir Feature Flags kennengelernt. Neben dem grundlegenden Prinzip haben wir uns auch Anwendungsfälle angeschaut, in denen diese uns unterstützen können. Die vier Kategorien Release, Betrieb, Berechtigungen und Experimente unterscheiden sich dabei vor allem in den Dimensionen Länge der Existenz des Feature Flags und Höhe der Dynamik zur Entscheidung des Zustands.

Im Anschluss haben wir mit FF4j und Togglz die meiner Meinung nach aktuell populärsten Bibliotheken für Feature Flags in Java angeschaut, die sich primär durch ihre Programmierschnittstellen unterscheiden. Mit Unleash gibt es noch eine dritte Option. Diese unterscheidet sich dabei vor allem durch einen dedizierten Service für Persistenz und Administration, fertige Clients auch für andere Programmiersprachen und die Out-of-the-Box-Unterstützung durch GitLab.

Natürlich muss nicht jede Anwendung Feature Flags nutzen. Und auch wenn wir uns für Feature Flags entscheiden, müssen wir nicht zwangsweise alle vier Kategorien damit bedienen. Wenn der Vorteil jedoch überwiegt, sollte im konkreten Kontext eine der Bibliotheken gewählt und eingesetzt werden. Von einer Eigenentwicklung würde ich persönlich absehen.