OpenFeature Providers

OpenFeature Java Provider

ConfigDirector Provider for the OpenFeature Java SDK

Introduction

The OpenFeature Java Provider is intended to be used in combination with the OpenFeature Java SDK. The provider wraps the ConfigDirector Java SDK.

The minimum Java version supported is 17.

The provider is thread safe. Register one instance when your application starts, and shut OpenFeature down when it stops. Evaluations read config state the provider already holds in memory, so they make no network calls on the request path.

Installation

The provider is published to the ConfigDirector Maven repository. Add the repository and the dependency to your build. If your Gradle build declares its repositories in settings.gradle.kts or settings.gradle, under dependencyResolutionManagement, the repository block goes there instead.

repositories {
    mavenCentral()
    exclusiveContent {
        forRepository {
            maven {
                name = "ConfigDirector"
                url = uri("https://maven.configdirector.com")
            }
        }
        filter {
            includeGroup("com.configdirector")
        }
    }
}

dependencies {
    implementation("com.configdirector:openfeature-server-provider:1.8.0")
}

The OpenFeature Java SDK and the ConfigDirector Java SDK are included as transitive dependencies.

Configure and initialize the client

  1. Create an instance of the provider using your server SDK key. You can retrieve a server SDK key for each environment under SDK keys in the dashboard's navigation panel.
  2. Set the OpenFeature provider.
  3. Get a client instance from OpenFeature.
ConfigDirectorSetup.java
import com.configdirector.openfeature.ConfigDirectorProvider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;

OpenFeatureAPI api = OpenFeatureAPI.getInstance();
try {
    api.setProviderAndWait(new ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"));
} catch (ProviderNotReadyError error) {
    System.err.println("Starting on in-code default values until ConfigDirector sends config: " + error.getMessage());
}
Client client = api.getClient();
Server SDK keys are secret values. Do not commit them to your source code repository. Provide them at runtime via environment variables instead.

When no config arrives in time

setProviderAndWait throws ProviderNotReadyError when no config arrives from ConfigDirector within the connection timeout, which is 3 seconds unless you set the connection's timeout, or when ConfigDirector rejects the server SDK key. The provider then reports the ERROR state, with a PROVIDER_ERROR event, and after a timeout it keeps connecting in the background. Until config arrives, flags resolve to their in-code default values with the PROVIDER_NOT_READY error code. Once it arrives, the provider reports READY, with one PROVIDER_READY event.

The example above catches the exception, logs it and starts on in-code default values. To start without waiting for ConfigDirector at all, set the provider with setProvider instead:

ConfigDirectorSetup.java
OpenFeatureAPI.getInstance().setProvider(new ConfigDirectorProvider("YOUR-SERVER-SDK-KEY"));

Before version 1.8.0 of the provider, setProviderAndWait returned after the timeout, and the provider reported READY while it served in-code default values.

Additional configuration options

Additional configuration options can be passed into the provider in the optional second argument of the constructor.

For example, the metadata can be provided like this:

ConfigDirectorSetup.java
import com.configdirector.openfeature.ConfigDirectorProvider;

ConfigDirectorProvider provider = new ConfigDirectorProvider(
    "YOUR-SERVER-SDK-KEY",
    options -> options.metadata("YOUR-APP-NAME", "1.0.2"));

The provider accepts the same configuration options as the Java SDK client, refer to the additional configuration options section of the Java SDK for a full list.

That includes running without a connection: built with new ConfigDirectorProvider(options -> ...) and no server SDK key, the provider serves the values you give it without connecting to ConfigDirector, and setProviderAndWait throws if those values cannot be loaded.

Shut down

Shutting OpenFeature down closes the provider, which closes its connections and reports any pending telemetry:

Main.java
OpenFeatureAPI.getInstance().shutdown();

Retrieve config values

To retrieve config values, use the OpenFeature client:

Main.java
boolean booleanValue = client.getBooleanValue("my-config-key", false);

String stringValue = client.getStringValue("my-string-config-key", "Default");

Each OpenFeature getter maps to a ConfigDirector config type:

OpenFeature getterConfigDirector config value
getBooleanValueBoolean
getStringValueString or enum
getIntegerValue, getLongValue, getDoubleValueNumber
getObjectValueJSON object or JSON array

For getObjectValue, the default value decides the expected shape: provide a structure for a JSON object, or a list for a JSON array.

Main.java
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.Value;

Value settings = client.getObjectValue("my-json-config-key", new Value(new ImmutableStructure()));
String theme = settings.asStructure().getValue("theme").asString();

For additional information regarding the OpenFeature client refer to the OpenFeature Java SDK documentation.

Evaluation details

The detailed getters of the OpenFeature client, such as getBooleanDetails, report why an evaluation produced the value that it did:

OutcomeReasonError code
A value was foundTARGETING_MATCH
The config carries no valueDEFAULT
The config key is unknownERRORFLAG_NOT_FOUND
No config state has arrived yetERRORPROVIDER_NOT_READY
The value does not match the requested typeERRORTYPE_MISMATCH

When a value was found, the variant is ConfigDirector's identifier for that value. In every other case the in-code default value is returned.

Context

The context can be provided as the third argument to value getter functions of the OpenFeature client. The OpenFeature Java provider evaluates targeting rules locally without additional network calls for different contexts.

Main.java
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.Value;
import java.util.Map;

// Any arbitrary traits which can be referenced in targeting rules
ImmutableStructure traits = new ImmutableStructure(Map.of("region", new Value("North America")));

EvaluationContext context = new ImmutableContext(
    "12345", // In OpenFeature, the targeting key represents the context's identifier
    Map.of("name", new Value("Example User"), "traits", new Value(traits)));

boolean booleanValue = client.getBooleanValue("my-config-key", false, context);

To evaluate flags for an entity other than a user, such as a tenant, set the context kind with a top-level kind attribute:

Main.java
EvaluationContext tenant = new ImmutableContext(
    "acme",
    Map.of(
        "kind", new Value("tenant"),
        "name", new Value("Acme Inc."),
        "traits", new Value(new ImmutableStructure(Map.of("plan", new Value("enterprise"))))));

boolean booleanValue = client.getBooleanValue("my-config-key", false, tenant);

Leaving kind out means user. A kind inside traits is an ordinary trait, not the context kind. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. A context with an invalid context kind is ignored: the SDK logs an error, and flags still resolve successfully, as if no context had been given. Context kinds are available from version 1.6.0 of the provider.

The evaluation context maps onto the ConfigDirector context as follows:

OpenFeature evaluation contextConfigDirector context
The targeting key, or otherwise an id attributeid
The top-level kind attributekind
The name attributename
The traits structure attributetraits
The boolean anonymous attributeanonymous

For additional information regarding the OpenFeature client refer to the OpenFeature Java SDK documentation.

Events

The provider emits PROVIDER_CONFIGURATION_CHANGED whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update:

Main.java
OpenFeatureAPI.getInstance().onProviderConfigurationChanged(
    details -> System.out.println("Configs updated: " + details.getFlagsChanged()));

When no config arrived in time at startup, the provider emits PROVIDER_READY once config arrives, as described in When no config arrives in time.

Test your code

Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test and nothing from ConfigDirector is involved. The OpenFeature Java SDK ships an in-memory provider for that, InMemoryProvider, which serves the values your test sets.

Each flag is a Flag built from its named variants and the defaultVariant it resolves to:

CheckoutTest.java
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.providers.memory.Flag;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;

class CheckoutTest {

    private final OpenFeatureAPI api = OpenFeatureAPI.getInstance();

    @AfterEach
    void shutOpenFeatureDown() {
        api.shutdown();
    }

    @Test
    void showsTheNewCheckout() {
        Map<String, Flag<?>> flags = new HashMap<>();
        flags.put("new-checkout", Flag.builder().variant("on", true).variant("off", false).defaultVariant("on").build());
        flags.put("max-items", Flag.builder().variant("default", 20).defaultVariant("default").build());
        InMemoryProvider provider = new InMemoryProvider(flags);
        api.setProviderAndWait(provider);
        Client client = api.getClient();

        assertTrue(client.getBooleanValue("new-checkout", false));
        assertEquals(20, client.getIntegerValue("max-items", 0));

        provider.updateFlags(Map.of(
            "new-checkout", Flag.builder().variant("on", true).variant("off", false).defaultVariant("off").build()));
        assertFalse(client.getBooleanValue("new-checkout", true));
    }
}

updateFlags adds or replaces the flags it is given, leaves the others as they are, and emits PROVIDER_CONFIGURATION_CHANGED with the keys it was given; updateFlag does the same for one flag. A flag can also carry a contextEvaluator, a function from the evaluation context to the value to serve, for a test of code that targets specific users. The OpenFeature API is one global, so shut it down after each test with api.shutdown(), as above, which lets the next test register its own provider.