Feature flags are not just tools for developers; they have far-reaching benefits across various business functions. They enable continuous deployment, incremental rollouts, A/B testing, risk mitigation, and feature experimentation. Their versatility extends beyond the coding realm to impact sales, customer support, marketing, and more. While feature flags may appear straightforward to implement and comprehend, it’s important to understand that they introduce additional code and complexity to your existing codebase. If a feature is eventually made accessible to all users, the additional code associated with it should be removed.
In this blog post, we’ll explore the transformative power of feature flags and introduce OpenFeature, a vendor-agnostic solution designed to work with any feature flag management tool or in-house solution.
Alright, let’s kick things off. In the vast sea of tools and software options, we have contenders like LaunchDarkly, Rollout, ConfigCat, Optimizely, Toggled, DevCycle, Unleash, and the list goes on. Personally, I lean towards standards and the freedom to experiment with various providers and solutions until I discover the one that best aligns with my needs. This is where OpenFeature steps in, as it allows you to explore different providers seamlessly.
Use case
Let’s delve into a practical use case. We aim to leverage feature flags to determine if a user, identified by their email address, has access to a new feature. I’ve chosen three providers—flagd, ConfigCat, and DevCycle—but for a comprehensive ecosystem overview, feel free to visit this page.
The example is written in Quarkus and you can find the source code here.
flagd
Let’s create a simple API for our use case.
@Path("/api")
public class PdAPI {
@Inject()
@FlagdQualifier
OpenFeatureAPI openFeatureAPI;
@GET()
@Path("/check/{email}")
@Produces(MediaType.APPLICATION_JSON)
public Response checkFeature(@PathParam("email") String email) {
final var client = openFeatureAPI.getClient();
client.addHooks(new PDTrackerHook());
var ctx = new MutableContext();
ctx.setTargetingKey("isfromproductdock");
ctx.add("Email", email);
if (client.getBooleanValue("isfromproductdock", false, ctx)) {
return Response.ok(new FeatureFlagResponse("This feature is enabled! User is from ProductDock.")).build();
}
return Response.ok(new FeatureFlagResponse("This feature is disabled! User is not from ProductDock.")).build();
}
}
record FeatureFlagResponse(String message) {}
We have a simple API with a GET method that, depending on the email provided, will return a message if the feature is enabled or disabled. To be able to inject and use OpenFeatureAPI, we need to create an instance and set our flagd provider. Don’t mind the Qualifier annotations, we will use different Qualifiers to distinguish between different implementations.
@ApplicationScoped
public class FlagdAPI {
@Produces
@FlagdQualifier
public OpenFeatureAPI getApi() {
final OpenFeatureAPI openFeatureAPI = OpenFeatureAPI.getInstance();
openFeatureAPI.setProvider(new FlagdProvider());
return openFeatureAPI;
}
}
Now that we have our instance in place, we need to run flagd. Visit this link to explore different installation options and their documentation. For this demo, we just run a Docker container:
docker run --rm -it --name flagd -p 8013:8013 -v $(pwd)/flagd-docker:/etc/flagd ghcr.io/open-feature/flagd:latest start --uri file:./etc/flagd/flagd.json
and provide the following config:
{
"flags": {
"isfromproductdock": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "off",
"targeting": {
"if": [
{
"$ref": "isFromProductDock"
},
"on",
null
]
}
}
},
"$evaluators": {
"isFromProductDock": {
"in": [
"@productdock.com",
{
"var": ["Email"]
}
]
}
}
}
It’s easy to understand, and flagd docs are awesome.
After running the application, we can use curl to test our API:
curl http://localhost:8080/api/check/example@productdock.com
or
curl http://localhost:8080/api/check/example@gmail.com
How difficult is it to change to another provider? Let’s try ConfigCat.
ConfigCat
We created a ConfigCat account and an SDK key that we will be using in our configuration.
Here is what the flag looks like in ConfigCat.
Next, we need to configure the OpenFeatureAPI instance to use ConfigCat as the provider.
@ApplicationScoped
public class ConfigCat {
@ConfigProperty(name = "configcat.sdkkey")
public String configCatKey;
@Produces
@ConfigCatQualifier
public OpenFeatureAPI getApi() {
final OpenFeatureAPI openFeatureAPI = OpenFeatureAPI.getInstance();
var configCat = ConfigCatProviderConfig.builder()
.sdkKey(configCatKey)
.build();
openFeatureAPI.setProviderAndWait(new ConfigCatProvider(configCat));
return openFeatureAPI;
}
}
Finally, we change the qualifier in our PdAPI so that our DI container knows which OpenFeatureAPI instance to use.
@Path("/api")
public class PdAPI {
@Inject()
// @FlagdQualifier
@ConfigCatQualifier
OpenFeatureAPI openFeatureAPI;
@GET()
@Path("/check/{email}")
@Produces(MediaType.APPLICATION_JSON)
public Response checkFeature(@PathParam("email") String email) {
final var client = openFeatureAPI.getClient();
client.addHooks(new PDTrackerHook());
var ctx = new MutableContext();
ctx.setTargetingKey("isfromproductdock");
ctx.add("Email", email);
if (client.getBooleanValue("isfromproductdock", false, ctx)) {
return Response.ok(new FeatureFlagResponse("This feature is enabled! User is from ProductDock.")).build();
}
return Response.ok(new FeatureFlagResponse("This feature is disabled! User is not from ProductDock.")).build();
}
}
record FeatureFlagResponse(String message) { }
Everything else stays the same.
DevCycle
For DevCycle, it’s the same story, almost. They use “email” instead of “Email” for their user object, but that’s an easy fix.
Before we continue, you should have a DevCycle account and your SDK key.
The following image shows the DevCycle UI for defining targeting rules.
Once again, we configure the OpenFeatureAPI instance, but now with the DevCycle provider.
@ApplicationScoped
public class DevCycle {
@ConfigProperty(name = "devcycle.sdkkey")
public String devCycleKey;
@Produces
@DevCycleQualifier
public OpenFeatureAPI getApi() {
final OpenFeatureAPI openFeatureAPI = OpenFeatureAPI.getInstance();
DevCycleLocalOptions options = DevCycleLocalOptions.builder().build();
DevCycleLocalClient devCycleClient = new DevCycleLocalClient(devCycleKey, options);
// This is wild. Should be handled by the client.
// https://github.com/DevCycleHQ/java-server-sdk/pull/111/files/4e21dc9a8f7d5d4d063528b355fc5c6125d9c78b#r1381707824
for (int i = 0; i < 10; i++) {
if (devCycleClient.isInitialized()) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
openFeatureAPI.setProvider(devCycleClient.getOpenFeatureProvider());
return openFeatureAPI;
}
}
We change the Qualifier and the email field casing for the context.
@Path("/api")
public class PdAPI {
@Inject()
// @FlagdQualifier
// @ConfigCatQualifier
@DevCycleQualifier
OpenFeatureAPI openFeatureAPI;
@GET()
@Path("/check/{email}")
@Produces(MediaType.APPLICATION_JSON)
public Response checkFeature(@PathParam("email") String email) {
final var client = openFeatureAPI.getClient();
client.addHooks(new PDTrackerHook());
var ctx = new MutableContext();
ctx.setTargetingKey("isfromproductdock");
// ctx.add("Email", email);
ctx.add("email", email);
if (client.getBooleanValue("isfromproductdock", false, ctx)) {
return Response.ok(new FeatureFlagResponse("This feature is enabled! User is from ProductDock.")).build();
}
return Response.ok(new FeatureFlagResponse("This feature is disabled! User is not from ProductDock.")).build();
}
}
record FeatureFlagResponse(String message) {
}
Hooks
You might have noticed the addHooks code. I like the idea of tracking, and the hooks are a perfect place to set this up. I haven’t done anything special, just wanted to see how it behaves.
public class PDTrackerHook implements BooleanHook {
private static final Logger LOG = Logger.getLogger(PDTrackerHook.class);
@Override
public void after(HookContext<Boolean> ctx, FlagEvaluationDetails<Boolean> details, Map<String, Object> hints) {
LOG.info(details.getFlagKey() + " : " + details.getValue());
}
}
More on hooks, visit this page.
What’s next
As you can see in the examples above, OpenFeature gives you flexibility with feature management providers. The team behind OpenFeature is actively working on further improvements, and I’m eagerly anticipating the exciting development ahead.
While this article primarily focuses on the server-side implementation, it’s important to note that OpenFeature also has robust client-side capabilities. If you’re keen to dive in, I recommend beginning with exploring their documentation. From there, feel free to explore any direction that aligns with your goals and preferences.
Originally published in the ProductDock blog section