OpenFeature provider for Featureflip, backed by the Featureflip Java server SDK.
dependencies {
implementation 'io.featureflip:featureflip-openfeature:0.1.1'
}<dependency>
<groupId>io.featureflip</groupId>
<artifactId>featureflip-openfeature</artifactId>
<version>0.1.1</version>
</dependency>The Featureflip Java SDK and the OpenFeature Java SDK come along as transitive dependencies.
import dev.openfeature.sdk.*;
import io.featureflip.openfeature.FeatureflipProvider;
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(new FeatureflipProvider("your-sdk-key"));
Client client = api.getClient();
boolean enabled = client.getBooleanValue("new-checkout", false,
new ImmutableContext("user-123", Map.of("plan", new Value("pro"))));Use setProviderAndWait rather than setProvider. The plain form returns before initialization has finished, so evaluations on the following lines can hand back their defaults.
Pass a FeatureFlagConfig to configure the underlying client:
api.setProviderAndWait(new FeatureflipProvider("your-sdk-key",
FeatureFlagConfig.builder()
.streaming(true)
.initTimeout(Duration.ofSeconds(5))
.build()));If the same process also evaluates flags through the Featureflip SDK directly, hand the provider your client so both paths share one connection rather than opening a second stream:
FeatureflipClient client = FeatureflipClient.get("your-sdk-key");
api.setProviderAndWait(new FeatureflipProvider(client));You keep ownership: shutdown() unsubscribes but leaves your client open.
OpenFeature's targetingKey becomes Featureflip's user id, which is what percentage rollouts bucket on and what userId/user_id targeting rules read.
An explicit user_id or userId attribute takes precedence over targetingKey — a caller who sets both meant the explicit one — and between the two spellings, user_id wins. Every other attribute is passed through for targeting rules and segments to match on.
EvaluationContext context = new MutableContext("user-123")
.add("plan", "enterprise")
.add("seats", 42);| Featureflip | OpenFeature |
|---|---|
RULE_MATCH |
TARGETING_MATCH |
FALLTHROUGH |
DEFAULT |
FLAG_DISABLED |
DISABLED |
PREREQUISITE_FAILED |
PREREQUISITE_FAILED |
FLAG_NOT_FOUND |
ERROR + FLAG_NOT_FOUND |
ERROR |
ERROR + GENERAL |
PREREQUISITE_FAILED is not a standard OpenFeature reason. Reasons are open strings, so it is surfaced verbatim rather than mislabelled as DEFAULT or DISABLED — the same choice the Node, .NET, Python and Go providers made. It is not an error: an unmet prerequisite serves the flag's off variation, which is a real value your code should receive.
Where a flag's evaluation carries them, ruleId and prerequisiteKey are attached as flag metadata.
A flag whose value is not of the type you asked for resolves to your default with TYPE_MISMATCH, distinctly from a genuine evaluation failure.
- Integer reads accept a whole number written in decimal form. JSON does not distinguish
1from1.0, so an integer flag stays readable however its value happened to be serialized. - Object reads accept objects and arrays only. A string, number or boolean resolves to your default with
TYPE_MISMATCH— reaching for an object value means you want structure.
The provider emits PROVIDER_CONFIGURATION_CHANGED when flag configuration changes, carrying the affected flag keys:
client.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details ->
log.info("flags changed: {}", details.getFlagsChanged()));The initial flag load is not reported — a cold start is not a change; OpenFeature signals that with PROVIDER_READY.
client.track("checkout-completed", context,
new MutableTrackingEventDetails(99.99).add("currency", "USD"));Featureflip counts each event name, per environment. The tracking event details are transmitted with the event but are not stored, and event counts are not surfaced in the app or API.
The numeric value is optional and is omitted when absent rather than recorded as zero.
A failed initial flag load does not fail initialization. The SDK keeps retrying in the background and evaluations serve your defaults until flags arrive, so a transient outage at startup degrades rather than takes the provider down. The timeout is logged.
Java 11 or higher.
Apache-2.0