OpenFeature Java Provider
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")
}
repositories {
mavenCentral()
exclusiveContent {
forRepository {
maven {
name = 'ConfigDirector'
url = 'https://maven.configdirector.com'
}
}
filter {
includeGroup 'com.configdirector'
}
}
}
dependencies {
implementation 'com.configdirector:openfeature-server-provider:1.8.0'
}
<repositories>
<repository>
<id>configdirector</id>
<url>https://maven.configdirector.com</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
<dependency>
<groupId>com.configdirector</groupId>
<artifactId>openfeature-server-provider</artifactId>
<version>1.8.0</version>
</dependency>
The OpenFeature Java SDK and the ConfigDirector Java SDK are included as transitive dependencies.
Configure and initialize the client
- Create an instance of the provider using your server SDK key. You can retrieve a server SDK key for each environment under
SDK keysin the dashboard's navigation panel. - Set the OpenFeature provider.
- Get a client instance from OpenFeature.
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();
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:
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:
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:
OpenFeatureAPI.getInstance().shutdown();
Retrieve config values
To retrieve config values, use the OpenFeature client:
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 getter | ConfigDirector config value |
|---|---|
getBooleanValue | Boolean |
getStringValue | String or enum |
getIntegerValue, getLongValue, getDoubleValue | Number |
getObjectValue | JSON 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.
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:
| Outcome | Reason | Error code |
|---|---|---|
| A value was found | TARGETING_MATCH | |
| The config carries no value | DEFAULT | |
| The config key is unknown | ERROR | FLAG_NOT_FOUND |
| No config state has arrived yet | ERROR | PROVIDER_NOT_READY |
| The value does not match the requested type | ERROR | TYPE_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.
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:
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 context | ConfigDirector context |
|---|---|
The targeting key, or otherwise an id attribute | id |
The top-level kind attribute | kind |
The name attribute | name |
The traits structure attribute | traits |
The boolean anonymous attribute | anonymous |
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:
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:
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.