OpenFeature – one flag to rule them all!

Feature flags and where to find them

What are feature flags?

To keep it simple it’s a way to change the system behaviour at runtime without hassle.

Note: And if this “refresher” was the only theoretical thing you wanted to know or if you already use feature flags and want to jump straight to code/ OpenFeature demo, feel free to do so; otherwise, please bear with me for another couple of text pages.

To keep it on a more scientific side, it’s a software development technique that allows enabling, disabling or changing the behaviour of certain features or code paths in a product or a service at runtime, without modifying the source code or re-deployment/service interruption (https://www.dynatrace.com/news/blog/feature-flags-with-openfeature-and-dynatrace/, https://www.youtube.com/watch?v=euYhIn4leW0).

Depending on your context or framework in use, feature flags might also be called feature toggles (https://martinfowler.com/articles/feature-toggles.html).

As the name suggests, initially feature flags were thought of more as a real switch, similar to the light on/off switch. Common usages of it I’ve seen and used so far in the wild include

  • kill switch like functionality (e.g. stop all data processing immediately in case master data is broken — to avoid further data corruption)
  • maintenance mode switch (e.g. for operational purposes during maintenance windows)
  • feature paywall (e.g. if premium subscription available, then export to certain additional formats is possible)

Although last case is already more sophisticated, since it includes some dynamic runtime parameter evaluation (identifying the user, checking the subscription), it still evaluates from a binary choice to a boolean value.

Obviously, once we find the possibility of getting a yes/no feature flag evaluation handy, we are willing to go a step further and evaluate a feature flag’s value to an arbitrary (pre-configured) integer, string or object value.

Common usage pattern for above enum-based evaluation could be delivering a future version of a product or service to be part of the production version but only usable if the feature flag is set accordingly. Imagine different process (e.g. based on legislation rules) versions with a fixed valid-from/valid-to date, that affect the service functionality and must be delivered and tested upfront together with the old versions to be soon deprecated.

Finally, another common scenario for using feature flags (in particularly, in enterprise product development) is A/B testing (https://en.wikipedia.org/wiki/A/B_testing). Configuring the feature flag(s) to return (be evaluated) to either A or B (or C, or D) at random at predefined split ratios is a powerful tool to run controlled experiments.

What does the 12-factor,K8s,fox,X say?

“Wait!”, you might say, but doesn’t it violate the 12-factor app (https://12factor.net/) manifest? Don’t we already have Canary Deployment (https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#canary-deployment) to support our DevOps teams?

Well, yes. And no. The world is not just black or white.

First, there are different applications. Some of them are monoliths, other microservices, few stateless lambdas, scripts, self-contained services, you name it.

Second, those don’t necessarily run on Kubernetes (or any other orchestrator, for that matter) – many still run on bare metal/ application servers.

Third, who said DevOps? Depending on your relation to the Ops department, duration of the pipelines (which take at least 1-2 hours in best case, with artifacts being scanned before every deployment as required by Security department), pre-defined maintenance windows (from once a week to twice a year for regular changes) and many other factors — every single configuration change on production systems might require weeks and months of preparation.

Forth.., I think you got the point. Sometimes changing the behaviour of the application without changing the application itself is the only way to progress.

Creating the 15th competing standard?

If “software development technique” sounds rather Linkedin-ish for a simple if statement you would definitely imagine behind an externally controlled on/off switch in your code, you’re not wrong!

It’s finally source code time! You will see, that it’s not hard to start having fun with feature flags, nor with OpenFeature!

Let’s build some simple feature flags on real-world examples up to the point of what would be worth calling a standard!

I’ll use JBang and SpringBoot for the demos, and I’ll take pizza bakery as homage to the observability training by my colleague Enis Spahi (https://www.linkedin.com/posts/enisspahi_munich-openvalue-observability-activity-7382350810837348352-cg0u).

Note: code is also available at https://github.com/coiouhkc/article-javapro-openfeature

Note: the state of the feature flags used will be kept in-memory for most of the demos to keep them small and to the point. I will only point out/ sketch a way to persist the state, where applicable.

Let’s set the scene — it’s a mix of 1962 and 2026. There is Java and Internet, JAVAPRO and LLMs, but there is no Pizza Hawaii yet. You’re are a pizza bakery owner, you have a new recipe and you want to test, whether there’s demand for it.

Demo1 – custom features + custom API

We start with a very simple solution, you don’t need to be a rocket scientist or Kubernetes expert to do it:

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3

package me.abratuhi.demo.openfeature;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@SpringBootApplication
public class demo1 {

	private static final String HAWAII = "Hawaii";

	private static final String FEATURE_PIZZA_HAWAII = "list-pizza-hawaii";

	private final Map<String, Boolean> featureState = new HashMap<>(Map.of(FEATURE_PIZZA_HAWAII, Boolean.FALSE));

	public static void main(String[] args) {
		SpringApplication.run(demo1.class, args);
	}

	@RestController
	public class PizzaController {

		private static final String HAWAII = "Hawaii";

		private static final Set<Pizza> PIZZAS = Set.of(new Pizza("Margherita"), new Pizza("Marinara"),
				new Pizza("Capricciosa"),
				new Pizza("Quattro Formaggi"), new Pizza(HAWAII));

		@GetMapping("/pizza")
		public ResponseEntity<Pizzas> getPizzas() {
			ArrayList<Pizza> pizzas = new ArrayList<>(PIZZAS);
			if (!featureState.get(FEATURE_PIZZA_HAWAII)) {
				pizzas.removeIf(pizza -> pizza.name().equals(HAWAII));
			}
			return ResponseEntity.ok(new Pizzas(pizzas));
		}

		@PostMapping("/pizza")
		public ResponseEntity<Pizza> orderPizza(@RequestBody Pizza pizza) {
			if (!PIZZAS.contains(pizza)) {
				return ResponseEntity.notFound().build();
			}
			if (pizza.name().equals(HAWAII)
					&& !featureState.get(FEATURE_PIZZA_HAWAII)) {
				return ResponseEntity.badRequest().body(new Pizza("Very bad taste!"));
			}
			return ResponseEntity.ok(pizza);
		}

		public record Pizzas(List<Pizza> pizzas) {
		}

		public record Pizza(String name) {
		}
	}

	@RestController
	public class FeatureController {

		@GetMapping("/feature")
		public ResponseEntity<Features> getFeatures() {
			return ResponseEntity.ok(new Features(featureState.entrySet()
				.stream()
				.map(kv -> new Feature(kv.getKey(), kv.getValue()))
				.collect(Collectors.toSet())));
		}

		@PutMapping("/feature")
		public ResponseEntity<Features> setFeature(@RequestBody Feature feature) {
			if (featureState.containsKey(feature.name())) {
				featureState.put(feature.name(), feature.state());
			}
			return ResponseEntity.ok(new Features(featureState.entrySet()
				.stream()
				.map(kv -> new Feature(kv.getKey(), kv.getValue()))
				.collect(Collectors.toSet())));
		}

		public record Features(Set<Feature> features) {
		}

		public record Feature(String name, boolean state) {
		}
	}

}
curl http://localhost:8080/pizza 

###

curl -X POST -H "Content-Type: application/json" http://localhost:8080/pizza -d '{"name": "Hawaii"}'

###

curl http://localhost:8080/feature

###

curl -X PUT -H "Content-Type: application/json" http://localhost:8080/feature -d '{"name": "list-pizza-hawaii", "state": true}'

Nothing too fancy, right?

PizzaController, which only lists and bakes Pizza Hawaii, if the list-pizza-hawaii flag is enabled; and FeatureController, that allows setting the said flag.

And just like that, your (as the owner) can analyze the turnover impact of offering Pizza Hawaii.

Persistence note: none necessary? Putting the state of the map into a file or a database should not cause any problems.

Demo2 – custom features + MBeans

But “I don’t want to expose my FeatureController” or “We must split business and operational concerns in API” o.s.?

For starters, you could configure security and/or access control. And if you have really set you mind on not using HTTP REST-like API to manage your feature flags, but still having some kind of standard interaction interface — here’s a good oldie for you, MBeans!

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3

package me.abratuhi.demo.openfeature;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.annotation.PostConstruct;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.management.MBeanServer;
import javax.management.ObjectName;

@SpringBootApplication
public class demo2 {

	private FeaturesMBean featureManager = new Features();

	public static void main(String[] args) {
		SpringApplication.run(demo2.class, args);
	}

	@PostConstruct
	public void exposeFeatureManager() throws Exception {
		MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
		ObjectName name = new ObjectName("me.abratuhin.demo.openfeature:type=Features");
		mbs.registerMBean(featureManager, name);
	}

	@RestController
	public class PizzaController {

		private static final String HAWAII = "Hawaii";

		private static final Set<Pizza> PIZZAS = Set.of(new Pizza("Margherita"), new Pizza("Marinara"),
				new Pizza("Capricciosa"),
				new Pizza("Quattro Formaggi"), new Pizza(HAWAII));

		@GetMapping("/pizza")
		public ResponseEntity<Pizzas> getPizzas() {
			ArrayList<Pizza> pizzas = new ArrayList<>(PIZZAS);
			if (!featureManager.getFeatureState(FeaturesMBean.FEATURE_PIZZA_HAWAII)) {
				pizzas.removeIf(pizza -> pizza.name().equals(HAWAII));
			}
			return ResponseEntity.ok(new Pizzas(pizzas));
		}

		@PostMapping("/pizza")
		public ResponseEntity<Pizza> orderPizza(@RequestBody Pizza pizza) {
			if (!PIZZAS.contains(pizza)) {
				return ResponseEntity.notFound().build();
			}
			if (pizza.name().equals(HAWAII) && !featureManager.getFeatureState(FeaturesMBean.FEATURE_PIZZA_HAWAII)) {
				return ResponseEntity.badRequest().body(new Pizza("Very bad taste!"));
			}
			return ResponseEntity.ok(pizza);
		}

		public record Pizzas(List<Pizza> pizzas) {
		}

		public record Pizza(String name) {
		}
	}

	public interface FeaturesMBean {
		public static final String FEATURE_PIZZA_HAWAII = "list-pizza-hawaii";

		boolean getFeatureState(String featureName);

		void setFeatureState(String featureName, boolean featureValue);
	}

	public class Features implements FeaturesMBean {

		private final Map<String, Boolean> featureState = new HashMap<>(Map.of(FEATURE_PIZZA_HAWAII, Boolean.FALSE));

		@Override
		public boolean getFeatureState(String featureName) {
			return featureState.getOrDefault(featureName, false);
		}

		@Override
		public void setFeatureState(String featureName, boolean featureValue) {
			if (featureState.containsKey(featureName)) {
				featureState.put(featureName, featureValue);
			}
		}

	}
}
curl http://localhost:8080/pizza 

###

curl -X POST -H "Content-Type: application/json" http://localhost:8080/pizza -d '{"name": "Hawaii"}'

PizzaController is still there, FeatureController is now gone replaced by (still boilerplate-ish) MBean (https://docs.oracle.com/javase/tutorial/jmx/mbeans/index.html) initialization and registration code. With the help of tools like VisualVM (https://visualvm.github.io/, with the MBeans Tab Plug-In) you can easily access and manage your feature flags.
Side note: it really pays off to get yourself familiarized with the JMX API — with tools like JMX Exporter (https://github.com/prometheus/jmx_exporter) it will bring your monitoring/observability skills to the next level.

Persistence note: more or less same as in the first demo. You need to make sure to write-through the changes (see https://docs.oracle.com/javase/tutorial/jmx/notifs/index.html) and initialize the values on startup, but it’s nothing out of the ordinary.

Demo3 – Togglz

“The best code is no code at all” — Jeff Atwood.

“Less is more” — https://en.wikipedia.org/wiki/Less_is_more.

Both above solutions feel a bit too verbose and boilerplate-ish — it’s time to use a framework and remove the waste!

Togglz (https://www.togglz.org/) has been there for a while, version 1.0.X having been released in June 2012. It has great features (including Togglz Console, providing the feature flag management UI).

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3
//DEPS org.togglz:togglz-spring-boot-starter:4.4.0
//DEPS org.togglz:togglz-console-spring-boot-starter:4.4.0

package me.abratuhi.demo.openfeature;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.togglz.core.Feature;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.annotation.Label;
import org.togglz.core.context.FeatureContext;
import org.togglz.core.manager.EnumBasedFeatureProvider;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.manager.TogglzConfig;
import org.togglz.core.repository.StateRepository;
import org.togglz.core.repository.mem.InMemoryStateRepository;
import org.togglz.core.spi.FeatureProvider;
import org.togglz.core.user.FeatureUser;
import org.togglz.core.user.SimpleFeatureUser;
import org.togglz.core.user.UserProvider;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.management.MBeanServer;
import javax.management.ObjectName;

@SpringBootApplication
public class demo3 {

	public static void main(String[] args) {
		SpringApplication.run(demo3.class, args);
	}

	@SuppressWarnings("unchecked")
	@Bean
	public FeatureProvider featureProvider() {
		return new EnumBasedFeatureProvider(MyFeatures.class);
	}

	/* don't do that on PROD! */
	@Bean
	public UserProvider getUserProvider() {
		return new UserProvider() {
			@Override
			public FeatureUser getCurrentUser() {
				return new SimpleFeatureUser("admin", true);
			}
		};
	}

	public enum MyFeatures implements Feature {

		@Label("list-pizza-hawaii")
		FEATURE_LIST_PIZZA_HAWAII;

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

	@RestController
	public class PizzaController {

		private static final String HAWAII = "Hawaii";

		private static final Set<Pizza> PIZZAS = Set.of(new Pizza("Margherita"), new Pizza("Marinara"),
				new Pizza("Capricciosa"),
				new Pizza("Quattro Formaggi"), new Pizza(HAWAII));
		@Autowired
		private FeatureManager featureManager;

		@GetMapping("/pizza")
		public ResponseEntity<Pizzas> getPizzas() {
			ArrayList<Pizza> pizzas = new ArrayList<>(PIZZAS);
			if (!featureManager.isActive(MyFeatures.FEATURE_LIST_PIZZA_HAWAII)) {
				pizzas.removeIf(pizza -> pizza.name().equals(HAWAII));
			}
			return ResponseEntity.ok(new Pizzas(pizzas));
		}

		@PostMapping("/pizza")
		public ResponseEntity<Pizza> orderPizza(@RequestBody Pizza pizza) {
			if (!PIZZAS.contains(pizza)) {
				return ResponseEntity.notFound().build();
			}
			if (pizza.name().equals(HAWAII)
					&& !featureManager.isActive(MyFeatures.FEATURE_LIST_PIZZA_HAWAII)) {
				return ResponseEntity.badRequest().body(new Pizza("Very bad taste!"));
			}
			return ResponseEntity.ok(pizza);
		}

		public record Pizzas(List<Pizza> pizzas) {
		}

		public record Pizza(String name) {
		}
	}
}

curl http://localhost:8080/pizza 

###

curl -X POST -H "Content-Type: application/json" http://localhost:8080/pizza -d '{"name": "Hawaii"}'

Again, PizzaController is there, but once you ignore the configuration parts made specifically for the demo purposes, the only feature flag relevant part is the definition of the feature flag itself!

Persistence note: it’s getting interesting now. We’re using the default InMemoryStateRepository, but there is more out-of-the-box! FileBasedStateRepository, JDBCStateRepository, even CachingStateRepository and some more (see https://www.togglz.org/documentation/repositories)!

Demo4 – Togglz + Console + custom activation strategy

Now, that we have have a framework, let’s explore its pros and cons.

Apart from the console and state repositories, there are also activation strategies (https://www.togglz.org/documentation/activation-strategies). In addition to the standard ones (e.g. Username, Gradual Rollout, Release Date) supporting the most common use cases listed above, one can also define custom ones.

So in the hypothetical scenario Italy would be able to ban Pizza Hawaii world-wide except for its origin country (Canada), our custom activation strategy could like this

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3
//DEPS org.togglz:togglz-spring-boot-starter:4.4.0
//DEPS org.togglz:togglz-console-spring-boot-starter:4.4.0

package me.abratuhi.demo.openfeature;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.annotation.RequestScope;
import org.togglz.core.Feature;
import org.togglz.core.activation.Parameter;
import org.togglz.core.activation.ParameterBuilder;
import org.togglz.core.annotation.ActivationParameter;
import org.togglz.core.annotation.DefaultActivationStrategy;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.annotation.Label;
import org.togglz.core.context.FeatureContext;
import org.togglz.core.manager.EnumBasedFeatureProvider;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.manager.TogglzConfig;
import org.togglz.core.repository.FeatureState;
import org.togglz.core.repository.StateRepository;
import org.togglz.core.repository.mem.InMemoryStateRepository;
import org.togglz.core.spi.ActivationStrategy;
import org.togglz.core.spi.FeatureProvider;
import org.togglz.core.user.FeatureUser;
import org.togglz.core.user.SimpleFeatureUser;
import org.togglz.core.user.UserProvider;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.management.MBeanServer;
import javax.management.ObjectName;

@SpringBootApplication
public class demo4 {

	public static void main(String[] args) {
		SpringApplication.run(demo4.class, args);
	}

	@SuppressWarnings("unchecked")
	@Bean
	public FeatureProvider featureProvider() {
		return new EnumBasedFeatureProvider(MyFeatures.class);
	}

	/* don't do that on PROD! */
	@Bean
	public UserProvider getUserProvider() {
		return new UserProvider() {
			@Override
			public FeatureUser getCurrentUser() {
				return new SimpleFeatureUser("admin", true);
			}
		};
	}

	public static enum MyFeatures implements Feature {

		@Label("list-pizza-hawaii")
		@EnabledByDefault
		@DefaultActivationStrategy(id = LocationActivationStrategy.ID, parameters = {
				@ActivationParameter(name = LocationActivationStrategy.PARAMETER_LOCATION, value = "ca") })
		FEATURE_LIST_PIZZA_HAWAII;

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

	@RequestScope
	@Component
	class LocationHeaderHolder {
		private String location;

		public String getLocation() {
			return this.location;
		}

		public void setLocation(String location) {
			this.location = location;
		}
	}

	@Component
	class LocationActivationStrategy implements ActivationStrategy {
		private static final String ID = "location";
		private static final String PARAMETER_LOCATION = "location";

		private LocationHeaderHolder locationHeaderHolder;

		public LocationActivationStrategy(LocationHeaderHolder locationHeaderHolder) {
			this.locationHeaderHolder = locationHeaderHolder;
		}

		@Override
		public String getId() {
			return "location";
		}

		@Override
		public String getName() {
			return "Location header strategy";
		}

		@Override
		public boolean isActive(FeatureState featureState, FeatureUser user) {
			String configuredLocation = featureState.getParameter(PARAMETER_LOCATION);
			String currentLocation = locationHeaderHolder.getLocation();
			return configuredLocation.equalsIgnoreCase(currentLocation);
		}

		@Override
		public Parameter[] getParameters() {
			return new Parameter[] {
					ParameterBuilder.create(PARAMETER_LOCATION).label("Location of pizzeria")
			};
		}

	}

	@RestController
	class PizzaController {

		private static final String HAWAII = "Hawaii";

		private static final Set<Pizza> PIZZAS = Set.of(new Pizza("Margherita"), new Pizza("Marinara"),
				new Pizza("Capricciosa"),
				new Pizza("Quattro Formaggi"), new Pizza(HAWAII));
		@Autowired
		private FeatureManager featureManager;
		@Autowired
		private LocationHeaderHolder locationHeaderHolder;

		@GetMapping("/pizza")
		public ResponseEntity<Pizzas> getPizzas(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location) {
			locationHeaderHolder.setLocation(location);

			ArrayList<Pizza> pizzas = new ArrayList<>(PIZZAS);
			if (!featureManager.isActive(MyFeatures.FEATURE_LIST_PIZZA_HAWAII)) {
				pizzas.removeIf(pizza -> pizza.name().equals(HAWAII));
			}
			return ResponseEntity.ok(new Pizzas(pizzas));
		}

		@PostMapping("/pizza")
		public ResponseEntity<Pizza> orderPizza(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location,
				@RequestBody Pizza pizza) {
			locationHeaderHolder.setLocation(location);

			if (!PIZZAS.contains(pizza)) {
				return ResponseEntity.notFound().build();
			}
			if (pizza.name().equals(HAWAII)
					&& !featureManager.isActive(MyFeatures.FEATURE_LIST_PIZZA_HAWAII)) {
				return ResponseEntity.badRequest().body(new Pizza("Very bad taste!"));
			}
			return ResponseEntity.ok(pizza);
		}

		public record Pizzas(List<Pizza> pizzas) {
		}

		public record Pizza(String name) {
		}
	}

}

curl http://localhost:8080/pizza 

###

curl -H "x-pizzeria-location: ca" http://localhost:8080/pizza 

###

curl -H "x-pizzeria-location: de" http://localhost:8080/pizza 

###

curl -X POST -H "Content-Type: application/json" http://localhost:8080/pizza -d '{"name": "Hawaii"}'

###

curl -X POST -H "Content-Type: application/json" -H "x-pizzeria-location: ca" http://localhost:8080/pizza -d '{"name": "Hawaii"}'

We use x-pizzeria-location header to store it’s value in a @RequestScoped LocationHolder bean to only activate the list-pizza-hawaii feature for customers coming from Canada (ca).

Persistence note: same as previous one.

Demo5 – OpenFeature

Now it’s time to talk about the cons of Togglz.

First, as a huge fan of Quarkus, I was rather disappointed not to get Togglz + Togglz Console to work on the first try. Luckily, there is now a Quarkiverse extension https://docs.quarkiverse.io/quarkus-flags/dev/. Also luckily (arguable) being a stubborn fan of Quarkus, I managed to get it working on the third or forth attempt (see https://github.com/coiouhkc/demo-quarkus-togglz), using https://quarkus.io/guides/http-reference#servlet-config, quarkus-undertow, web.xml and a pinch of black magic.

Second, it’s baked into the application, meaning it makes it hard to use in a horizontally scaled or multi-app setup consistently. It’s not stand-alone, which means higher images size and cpu/memory footprint.

That’s where OpenFeature (https://openfeature.dev/) comes in handy.

OpenFeature is a relatively new specification (version 0.1.0 released in July 2022, see https://github.com/open-feature/spec/releases/tag/v0.1.0), which defines a vendor-agnostic community-drive API for feature flagging.

It already offers a wide range of providers and clients for Java (and other languages) and is listed on the CNCF Landscape page (https://landscape.cncf.io/?item=observability-and-analysis–feature-flagging–openfeature), which is a deal breaker for many large companies.

Let’s see what we have to change in order to migrate from Togglz to OpenFeature.

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3
//DEPS dev.openfeature:sdk:1.20.1
//DEPS dev.openfeature.contrib.providers:flagd:0.11.19

package me.abratuhi.demo.openfeature;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import dev.openfeature.contrib.providers.flagd.Config;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
import jakarta.annotation.PostConstruct;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@SpringBootApplication
public class demo5 {

	private static final String FEATURE_PIZZA_HAWAII = "list-pizza-hawaii";

	private Client client;

	public static void main(String[] args) {
		SpringApplication.run(demo5.class, args);
	}

	@PostConstruct
	public void initOpenFeatureClient() {
		OpenFeatureAPI api = OpenFeatureAPI.getInstance();
		try {
			api.setProviderAndWait(new FlagdProvider(
					FlagdOptions.builder()
						.resolverType(Config.Resolver.RPC)
						.host("localhost")
						.port(8013)
						.build()));
		} catch (Exception e) {
			// handle initialization failure
			e.printStackTrace();
		}

		// create a client
		this.client = api.getClient();
	}

	@RestController
	public class PizzaController {

		private static final String HAWAII = "Hawaii";

		private static final Set<Pizza> PIZZAS = Set.of(new Pizza("Margherita"), new Pizza("Marinara"),
				new Pizza("Capricciosa"),
				new Pizza("Quattro Formaggi"), new Pizza(HAWAII));

		@GetMapping("/pizza")
		public ResponseEntity<Pizzas> getPizzas(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location) {
			ArrayList<Pizza> pizzas = new ArrayList<>(PIZZAS);
			if (!client.getBooleanValue(FEATURE_PIZZA_HAWAII, Boolean.FALSE,
					new ImmutableContext(Map.of("location", new Value(location))))) {
				pizzas.removeIf(pizza -> pizza.name().equals(HAWAII));
			}
			return ResponseEntity.ok(new Pizzas(pizzas));
		}

		@PostMapping("/pizza")
		public ResponseEntity<Pizza> orderPizza(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location,
				@RequestBody Pizza pizza) {
			if (!PIZZAS.contains(pizza)) {
				return ResponseEntity.notFound().build();
			}
			if (pizza.name().equals(HAWAII) && !client.getBooleanValue(FEATURE_PIZZA_HAWAII, Boolean.FALSE,
					new ImmutableContext(Map.of("location", new Value(location))))) {
				return ResponseEntity.badRequest().body(new Pizza("Very bad taste!"));
			}
			return ResponseEntity.ok(pizza);
		}

		public record Pizzas(List<Pizza> pizzas) {
		}

		public record Pizza(String name) {
		}
	}
}
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{}}' -H "Content-Type: application/json"

###

curl http://localhost:8080/pizza 

###

curl -H "x-pizzeria-location: ca" http://localhost:8080/pizza 

Other dependencies (sdk, contrib.provider), but our PizzaController stayed almost same!

You must have noticed, that I’ve picked flagd (https://flagd.dev/) as provider, since its configuration is text-based and more suitable for the article, but you’re free to play around with any other provider (Unleash, Flagsmith, FlipIt, etc.)!

services:
  flagd:
    image: ghcr.io/open-feature/flagd:v0.13.2
    volumes:
      - ./flagd/:/etc/flagd/
    tty: true
    stdin_open: true
    command: start --uri file:/etc/flagd/flags.flagd.json
    ports:
      - 8013:8013
{
  "$schema": "https://flagd.dev/schema/v0/flags.json",
  "flags": {
    "list-pizza-hawaii": {
      "state": "ENABLED",
      "defaultVariant": "no",
      "variants": {
        "yes": true,
        "no": false
      },
      "targeting": {
        "if": [
          {
            "in": [
              {
                "var": "location"
              },
              [
                "ca"
              ]
            ]
          },
          "yes"
        ]
      }
    }
  }
}

Persistence note: flagd supports multiple “Syncs” (see https://flagd.dev/concepts/syncs/), for this demo I chose to use file-based. Other persistence providers (e.g. Unleash) also support databases (Postgres) to store the data.

Demo6 – OpenFeature + multiple environments

This time we let OpenFeature shine — as mentioned previously, having feature flag provider run on its own has certain advantages.
In this case we add an evaluation dimension environment — for the case we have single OpenFeature provider instance (as a Service from the Ops Team) and we want to test the location based feature flag locally and on dev stage, before activating it on qa and production stages.

Note: The nested if statement of JsonLogic (https://json-logic.github.io/json-logic-engine/docs/logic/) might be hard to read, you can always test your configuration at the Playground.

Note: all hail open source! Obviously some kind of switch or pattern matching would be better, but it might take a while, see https://github.com/json-logic/json-logic-engine/issues/55 and https://github.com/orgs/json-logic/discussions/44. We might get it sooner or later.

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3
//DEPS dev.openfeature:sdk:1.20.1
//DEPS dev.openfeature.contrib.providers:flagd:0.11.19

package me.abratuhi.demo.openfeature;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import dev.openfeature.contrib.providers.flagd.Config;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
import jakarta.annotation.PostConstruct;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@SpringBootApplication
public class demo6 {

	private static final String FEATURE_PIZZA_HAWAII = "list-pizza-hawaii";

	private Client client;

	public static void main(String[] args) {
		SpringApplication.run(demo6.class, args);
	}

	@PostConstruct
	public void initOpenFeatureClient() {
		OpenFeatureAPI api = OpenFeatureAPI.getInstance();
		try {
			api.setProviderAndWait(new FlagdProvider(
					FlagdOptions.builder()
						.resolverType(Config.Resolver.RPC)
						.host("localhost")
						.port(8013)
						.build()));
		} catch (Exception e) {
			// handle initialization failure
			e.printStackTrace();
		}

		// create a client
		this.client = api.getClient();
	}

	@RestController
	public class PizzaController {

		@org.springframework.beans.factory.annotation.Value("${app.env:local}")
		private String env;

		private static final String HAWAII = "Hawaii";

		private static final Set<Pizza> PIZZAS = Set.of(new Pizza("Margherita"), new Pizza("Marinara"),
				new Pizza("Capricciosa"),
				new Pizza("Quattro Formaggi"), new Pizza(HAWAII));

		@GetMapping("/pizza")
		public ResponseEntity<Pizzas> getPizzas(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location) {
			ArrayList<Pizza> pizzas = new ArrayList<>(PIZZAS);
			if (!client.getBooleanValue(FEATURE_PIZZA_HAWAII, Boolean.FALSE,
					new ImmutableContext(Map.of("location", new Value(location), "env", new Value(env))))) {
				pizzas.removeIf(pizza -> pizza.name().equals(HAWAII));
			}
			return ResponseEntity.ok(new Pizzas(pizzas));
		}

		@PostMapping("/pizza")
		public ResponseEntity<Pizza> orderPizza(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location,
				@RequestBody Pizza pizza) {
			if (!PIZZAS.contains(pizza)) {
				return ResponseEntity.notFound().build();
			}
			if (pizza.name().equals(HAWAII) && !client.getBooleanValue(FEATURE_PIZZA_HAWAII, Boolean.FALSE,
					new ImmutableContext(Map.of("location", new Value(location), "env", new Value(env))))) {
				return ResponseEntity.badRequest().body(new Pizza("Very bad taste!"));
			}
			return ResponseEntity.ok(pizza);
		}

		public record Pizzas(List<Pizza> pizzas) {
		}

		public record Pizza(String name) {
		}
	}
}
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{}}' -H "Content-Type: application/json"

###

curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{"env": "qa"}}' -H "Content-Type: application/json"

###

curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{"env": "dev"}}' -H "Content-Type: application/json"

###

curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{"env": "dev", "location": "ca"}}' -H "Content-Type: application/json"

###

curl http://localhost:8080/pizza 

###

curl -H "x-pizzeria-location: ca" http://localhost:8080/pizza 
{
  "$schema": "https://flagd.dev/schema/v0/flags.json",
  "flags": {
    "list-pizza-hawaii": {
      "state": "ENABLED",
      "defaultVariant": "no",
      "variants": {
        "yes": true,
        "no": false
      },
      "targeting": {
        "if": [
          {
            "in": [
              {
                "var": "env"
              },
              [
                "qa",
                "prod"
              ]
            ]
          },
          "no",
          {
            "if": [
              {
                "in": [
                  {
                    "var": "location"
                  },
                  [
                    "ca"
                  ]
                ]
              },
              "yes",
              "no"
            ]
          }
        ]
      }
    }
  }
}

Note: docker-compose.yml omitted for brevity, please re-use the one from demo5

With provided configuration, if starting our Pizza Bakery app with APP_ENV="qa" jbang "demos/demo6/demo6.java" or APP_ENV="dev" jbang "demos/demo6/demo6.java" will either immediately evaluate to list-pizza-hawaii flag to false or take the value of the x-pizzeria-location header first.

Demo7 – OpenFeature + multiple environments + multiple apps

For the final demo of the day we taking a look at the microservice-like architecture — multiple environments, multiple services (we add a recipe provider), multiple flags.

Additionally to the list-pizza-hawaii flag we already played with enough, we now add list-pizza-diavolo flag for the brand new pizza recipe in our portfolio — this time as A/B test, listing it on the menu and recipes only 50% of the time.

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3
//DEPS dev.openfeature:sdk:1.20.1
//DEPS dev.openfeature.contrib.providers:flagd:0.11.19

package me.abratuhi.demo.openfeature;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import dev.openfeature.contrib.providers.flagd.Config;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
import jakarta.annotation.PostConstruct;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@SpringBootApplication
public class demo71 {

	private static final String FEATURE_PIZZA_HAWAII = "list-pizza-hawaii";

	private static final String FEATURE_PIZZA_DIAVOLO = "list-pizza-diavolo";

	private Client client;

	public static void main(String[] args) {
		SpringApplication.run(demo71.class, args);
	}

	@PostConstruct
	public void initOpenFeatureClient() {
		OpenFeatureAPI api = OpenFeatureAPI.getInstance();
		try {
			api.setProviderAndWait(new FlagdProvider(
					FlagdOptions.builder()
						.resolverType(Config.Resolver.RPC)
						.host("localhost")
						.port(8013)
						.build()));
		} catch (Exception e) {
			// handle initialization failure
			e.printStackTrace();
		}

		// create a client
		this.client = api.getClient();
	}

	@RestController
	public class PizzaController {

		@org.springframework.beans.factory.annotation.Value("${app.env:local}")
		private String env;

		private static final String HAWAII = "Hawaii";

		private static final String DIAVOLO = "Diavolo";

		private static final Set<Pizza> PIZZAS = Set.of(new Pizza("Margherita"), new Pizza("Marinara"),
				new Pizza("Capricciosa"),
				new Pizza("Quattro Formaggi"), new Pizza(HAWAII), new Pizza(DIAVOLO));

		@GetMapping("/pizza")
		public ResponseEntity<Pizzas> getPizzas(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location) {
			ArrayList<Pizza> pizzas = new ArrayList<>(PIZZAS);
			if (!client.getBooleanValue(FEATURE_PIZZA_HAWAII, Boolean.FALSE,
					new ImmutableContext(Map.of("location", new Value(location), "env", new Value(env))))) {
				pizzas.removeIf(pizza -> pizza.name().equals(HAWAII));
			}
			if (!client.getBooleanValue(FEATURE_PIZZA_DIAVOLO, Boolean.FALSE)) {
				pizzas.removeIf(pizza -> pizza.name().equals(DIAVOLO));
			}
			return ResponseEntity.ok(new Pizzas(pizzas));
		}

		@PostMapping("/pizza")
		public ResponseEntity<Pizza> orderPizza(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location,
				@RequestBody Pizza pizza) {
			if (!PIZZAS.contains(pizza)) {
				return ResponseEntity.notFound().build();
			}
			if (pizza.name().equals(HAWAII) && !client.getBooleanValue(FEATURE_PIZZA_HAWAII, Boolean.FALSE,
					new ImmutableContext(Map.of("location", new Value(location), "env", new Value(env))))) {
				return ResponseEntity.badRequest().body(new Pizza("Very bad taste!"));
			}
			if (pizza.name.equals(DIAVOLO) && !client.getBooleanValue(FEATURE_PIZZA_DIAVOLO, Boolean.FALSE)) {
				return ResponseEntity.badRequest().body(new Pizza("Sorry, mate, better luck next time."));
			}
			return ResponseEntity.ok(pizza);
		}

		public record Pizzas(List<Pizza> pizzas) {
		}

		public record Pizza(String name) {
		}
	}
}
///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS org.springframework.boot:spring-boot-starter-web:3.5.3
//DEPS dev.openfeature:sdk:1.20.1
//DEPS dev.openfeature.contrib.providers:flagd:0.11.19
//RUNTIME_OPTIONS -Dserver.port=8081

package me.abratuhi.demo.openfeature;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import dev.openfeature.contrib.providers.flagd.Config;
import dev.openfeature.contrib.providers.flagd.FlagdOptions;
import dev.openfeature.contrib.providers.flagd.FlagdProvider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;
import jakarta.annotation.PostConstruct;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@SpringBootApplication
public class demo72 {

	private static final String FEATURE_PIZZA_HAWAII = "list-pizza-hawaii";

	private static final String FEATURE_PIZZA_DIAVOLO = "list-pizza-diavolo";

	private Client client;

	public static void main(String[] args) {
		SpringApplication.run(demo72.class, args);
	}

	@PostConstruct
	public void initOpenFeatureClient() {
		OpenFeatureAPI api = OpenFeatureAPI.getInstance();
		try {
			api.setProviderAndWait(new FlagdProvider(
					FlagdOptions.builder()
						.resolverType(Config.Resolver.RPC)
						.host("localhost")
						.port(8013)
						.build()));
		} catch (Exception e) {
			// handle initialization failure
			e.printStackTrace();
		}

		// create a client
		this.client = api.getClient();
	}

	@RestController
	public class RecipeController {

		@org.springframework.beans.factory.annotation.Value("${app.env:local}")
		private String env;

		private static final String HAWAII = "Hawaii";

		private static final String DIAVOLO = "Diavolo";

		private static final List<Recipe> RECIPIES = List.of(new Recipe("Margherita", "Tomatoes, mozzarella, basil."),
				new Recipe("Marinara", "Tomato sauce, olive oil, oregano, garlic."),
				new Recipe("Capricciosa", "Ham, mushrooms, artichokes, egg."),
				new Recipe("Quattro Formaggi",
						"Mozzarella, Gorgonzola and two others [cheese] depending on the region."),
				new Recipe(HAWAII, " Pineapple, tomato sauce, mozzarella cheese, and either ham or bacon."), new Recipe(
						DIAVOLO, "Tomatoes, mozzarella, spicy salami, pork sausage, calamata olives, chili flakes."));

		@GetMapping("/recipe")
		public ResponseEntity<Recipies> getRecipies(
				@RequestHeader(value = "x-pizzeria-location", required = false) String location) {
			ArrayList<Recipe> recipes = new ArrayList<>(RECIPIES);
			if (!client.getBooleanValue(FEATURE_PIZZA_HAWAII, Boolean.FALSE,
					new ImmutableContext(Map.of("location", new Value(location), "env", new Value(env))))) {
				recipes.removeIf(recipe -> recipe.name().equals(HAWAII));
			}
			if (!client.getBooleanValue(FEATURE_PIZZA_DIAVOLO, Boolean.FALSE)) {
				recipes.removeIf(recipe -> recipe.name().equals(DIAVOLO));
			}
			return ResponseEntity.ok(new Recipies(recipes));
		}

		public record Recipies(List<Recipe> recipies) {
		}

		public record Recipe(String name, String text) {
		}
	}
}
curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{}}' -H "Content-Type: application/json"

###

curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{"env": "qa"}}' -H "Content-Type: application/json"

###

curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{"env": "dev"}}' -H "Content-Type: application/json"

###

curl -X POST "http://localhost:8013/flagd.evaluation.v1.Service/ResolveBoolean" \
  -d '{"flagKey":"list-pizza-hawaii","context":{"env": "dev", "location": "ca"}}' -H "Content-Type: application/json"

###

curl http://localhost:8080/pizza 

###

curl -H "x-pizzeria-location: ca" http://localhost:8080/pizza 

###

curl http://localhost:8081/recipe
{
  "$schema": "https://flagd.dev/schema/v0/flags.json",
  "flags": {
    "list-pizza-hawaii": {
      "state": "ENABLED",
      "defaultVariant": "no",
      "variants": {
        "yes": true,
        "no": false
      },
      "targeting": {
        "if": [
          {
            "in": [
              {
                "var": "env"
              },
              [
                "qa",
                "prod"
              ]
            ]
          },
          "no",
          {
            "if": [
              {
                "in": [
                  {
                    "var": "location"
                  },
                  [
                    "ca"
                  ]
                ]
              },
              "yes",
              "no"
            ]
          }
        ]
      }
    },
    "list-pizza-diavolo": {
      "state": "ENABLED",
      "defaultVariant": "no",
      "variants": {
        "yes": true,
        "no": false
      },
      "targeting": {
        "fractional": [
          {
            "cat": [
              {
                "var": "$flagd.timestamp"
              }
            ]
          },
          [
            "yes",
            50
          ],
          [
            "no",
            50
          ]
        ]
      }
    }
  }
}

Using this feature flag setup (given we have enough observability configured), we (as the pizzeria owner) can get insights into the pizza market dynamics:

  • how popular are our feature pizzas?
  • do our recipes get more popular, once the featured pizza was orderer?
  • do orders of the featured pizzas correlate?

and many more.

If you are adventurous and curious — give OpenFeature Tracking (https://openfeature.dev/docs/reference/concepts/tracking/) a try! It’s a recent addition to the specification to allow associating metrics and KPIs with feature flag evaluation context. Pull requests are welcome!

Yellow brick road

Are feature flags generally and OpenFeatures in particularly all rainbow and unicorns? Of course, not!

Though the technical/implementation part of it is pretty simple; the maintenance, operational and monitoring costs can be very high.

As noticed by many guides and articles about feature flagging (e.g. https://octopus.com/devops/feature-flags/feature-flag-best-practices/, https://netoff.dev/blog/2022-04-25-feature-flags-the-good-and-bad), testing the application, deprecating and removing obsolete flags can get very challenging very fast once the amount of feature flags grows (combinatorial explosion).

Nesting feature flags (if A, then B or C; else if X then Y otherwise Z) is also generally just asking for troubles.

Don’t be afraid to use it, but make it a conscious decision; use the powers OpenFeature wisely and we’ll meet if attend-same-conference or submit-a-pr evaluates to true.

Total
0
Shares
Previous Post

Simpler JVM Project Setup with Mill

Next Post

Vaadin + Quarkus: The New Approach for Enterprise Apps

Related Posts