Featureflip Java SDK

September 18, 2026 · View on GitHub

Java SDK for Featureflip - evaluate feature flags locally with near-zero latency.

Installation

Gradle

implementation 'io.featureflip:featureflip-java:2.10.0'

Maven

<dependency>
    <groupId>io.featureflip</groupId>
    <artifactId>featureflip-java</artifactId>
    <version>2.10.0</version>
</dependency>

Quick Start

import io.featureflip.client.FeatureflipClient;
import io.featureflip.client.EvaluationContext;

FeatureflipClient client = FeatureflipClient.get("your-sdk-key");
client.waitForInitialization();

boolean enabled = client.boolVariation("my-feature",
    EvaluationContext.builder("user-123").build(), false);

if (enabled) {
    System.out.println("Feature is enabled!");
}

client.close();

Lifetime: The client is designed to be used as a singleton. Calling FeatureflipClient.get() (or builder().build()) multiple times with the same SDK key returns handles sharing one underlying client — you cannot accidentally open duplicate streaming connections. For dependency injection, register it as a singleton bean.

Configuration

Pass a FeatureFlagConfig to get() when you need non-default options:

import io.featureflip.client.FeatureFlagConfig;

FeatureflipClient client = FeatureflipClient.get("your-sdk-key",
    FeatureFlagConfig.builder()
        .baseUrl("https://eval.featureflip.io")          // Evaluation API URL (default)
        .streaming(true)                                   // SSE for real-time updates (default)
        .pollInterval(Duration.ofSeconds(30))              // Polling interval if streaming=false
        .flushInterval(Duration.ofSeconds(30))             // Event flush interval
        .flushBatchSize(100)                               // Events per batch
        .initTimeout(Duration.ofSeconds(10))               // Max wait for initialization
        .connectTimeout(Duration.ofSeconds(5))             // HTTP connection timeout
        .readTimeout(Duration.ofSeconds(10))               // HTTP read timeout
        .build());

The SDK key can also be supplied through the FEATUREFLIP_SDK_KEY environment variable — pass null or a blank string to get() (or builder()) and it is read from there. A key passed explicitly always wins. If neither supplies one, get() throws IllegalArgumentException naming both routes.

Evaluation

EvaluationContext context = EvaluationContext.builder("user-123").build();

// Boolean flag
boolean enabled = client.boolVariation("feature-key", context, false);

// String flag
String tier = client.stringVariation("pricing-tier", context, "free");

// Integer flag
int limit = client.intVariation("rate-limit", context, 100);

// Double flag
double ratio = client.doubleVariation("rollout-ratio", context, 0.5);

// JSON flag
UiConfig config = client.jsonVariation("ui-config", context,
    new UiConfig("light"), UiConfig.class);

Detailed Evaluation

EvaluationDetail<Boolean> detail = client.boolVariationDetail(
    "feature-key", EvaluationContext.builder("123").build(), false);

System.out.println(detail.getValue());        // The evaluated value
System.out.println(detail.getReason());        // RULE_MATCH, FALLTHROUGH, FLAG_DISABLED, etc.
System.out.println(detail.getRuleId());        // Rule ID if reason is RULE_MATCH
System.out.println(detail.getErrorMessage());  // Error details if reason is ERROR

JSON flags have a detail accessor too. Read one as Object to get the served value in its plain Java shape (Map, List, String, Integer, Double, Boolean) without asserting a type up front:

EvaluationDetail<Object> detail = client.jsonVariationDetail(
    "ui-config", context, null, Object.class);

Reacting to Flag Changes

Subscribe to configuration changes to invalidate a cache, re-render, or log:

Runnable unsubscribe = client.onUpdate(flagKeys ->
    log.info("flags changed: {}", flagKeys));

// later
unsubscribe.run();

The listener receives the keys whose evaluated value may have moved, batched into one call per update. That includes flags dragged along by a change they do not themselves record: those targeting an edited segment, and those whose prerequisite moved.

The initial flag load does not fire -- a cold start is not a change. The listener runs on the SDK's streaming or polling thread, so it must not block; hand anything substantial to your own executor. Subscriptions are dropped when the client is closed, so a caller using try-with-resources need not unsubscribe.

Event Tracking

// Track custom events
client.track("checkout-completed",
    EvaluationContext.builder("123").build(),
    Map.of("total", 99.99));

// Record an identify event for analytics (does not affect flag evaluation)
client.identify(EvaluationContext.builder("123")
    .set("plan", "pro")
    .build());

// Anonymous: attributes, no identity. The event omits userId rather than
// sending an empty one. Use this instead of builder("") when there is no
// identity to carry — builder("") claims a present-but-empty identity.
client.identify(EvaluationContext.builder()
    .set("plan", "pro")
    .build());

// Force flush pending events
client.flush();

Resource Management

The client implements AutoCloseable for try-with-resources:

try (var client = FeatureflipClient.get("your-sdk-key")) {
    client.waitForInitialization();
    boolean enabled = client.boolVariation("feature", context, false);
}
// Automatically closed and flushed

Testing

Use forTesting() to create a client with predetermined flag values -- no network calls.

FeatureflipClient client = FeatureflipClient.forTesting(Map.of(
    "my-feature", true,
    "pricing-tier", "pro"
));

client.boolVariation("my-feature", context, false);     // true
client.stringVariation("pricing-tier", context, "free"); // "pro"
client.boolVariation("unknown", context, false);         // false (default)

Features

  • Local evaluation - Near-zero latency after initialization
  • Real-time updates - SSE streaming with automatic polling fallback
  • Change notifications - onUpdate listeners for configuration changes
  • Event tracking - Automatic batching and background flushing
  • Test support - forTesting() factory for deterministic unit tests
  • AutoCloseable - Works with try-with-resources
  • Thread-safe - Safe for concurrent access from multiple threads

Requirements

  • Java 11+

License

Apache-2.0