Skip to content

Java SDK Reference

TL;DR: Add io.featureflip:featureflip-java, call client.boolVariation("flag-key", context, false), and you’re evaluating flags — thread-safe and non-blocking.

The io.featureflip:featureflip-java library provides a thread-safe client for evaluating feature flags in Java applications. The SDK targets Java 11+ via Gradle toolchains, so it runs on every modern JVM — including LTS releases (11, 17, 21) and recent non-LTS builds — with no module-system gymnastics. Distribution is Maven Central under io.featureflip:featureflip-java.

Internally, the client uses OkHttp for HTTP and SSE streaming and Jackson for JSON serialization. Both are well-known, battle-tested libraries that integrate cleanly with Spring, Micronaut, Quarkus, and Dropwizard. The streaming connection runs on a dedicated background thread; if it drops, the SDK falls back to polling every 30 seconds while continuing to serve the last known flag values.

The client is built once per process via FeatureflipClient.builder().sdkKey(...).build() and shared across threads. All variation methods (boolVariation, stringVariation, intVariation, doubleVariation, jsonVariation) read from a ConcurrentHashMap and return without blocking, so you can call them from request handlers, schedulers, or message consumers without coordinating locks. The matching client implementation also powers the Android SDK, which adds Kotlin coroutines and lifecycle integration on top.

View source on GitHub

dependencies {
implementation("io.featureflip:featureflip-java:2.0.0")
}
dependencies {
implementation 'io.featureflip:featureflip-java:2.0.0'
}
<dependency>
<groupId>io.featureflip</groupId>
<artifactId>featureflip-java</artifactId>
<version>2.0.0</version>
</dependency>

The Featureflip Java SDK is designed to be used as a singleton across your application. Obtain the client via the static factory:

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

Or with custom configuration:

FeatureFlagConfig config = FeatureFlagConfig.builder()
.baseUrl("https://eval.featureflip.io")
.streaming(true)
.build();
FeatureflipClient client = FeatureflipClient.get("your-sdk-key", config);

Calling get() (or builder().build()) multiple times with the same SDK key returns handles that share a single underlying client — the SDK maintains a process-wide cache keyed by SDK key. You cannot accidentally open duplicate streaming connections by constructing multiple clients.

If you’re using a DI container like Spring Boot or Micronaut, register the client as a singleton bean:

@Configuration
public class FeatureflipConfig {
@Bean
public FeatureflipClient featureflipClient(@Value("${featureflip.sdk-key}") String sdkKey) {
return FeatureflipClient.get(sdkKey);
}
}

Even if you hand-roll a prototype-scoped or request-scoped registration, the factory’s deduplication ensures all resolves share one underlying client — the misregistration is neutralized, not catastrophic.

close() is refcounted: when multiple handles share one cached client, closing one handle does not shut down the shared background tasks. The real shutdown runs only when the last handle is closed. In a typical application this means the client lives for the entire process lifetime and is cleaned up automatically at shutdown via a try-with-resources block in main or via the DI container’s shutdown lifecycle.

The FeatureFlagConfig class is built via a builder with the following options:

OptionTypeDefaultDescription
baseUrlString"https://eval.featureflip.io"Base URL of the Evaluation API
connectTimeoutDuration5sTimeout for establishing connections
readTimeoutDuration10sTimeout for reading responses
streamingbooleantrueEnable SSE for real-time flag updates
pollIntervalDuration30sPolling interval (used when streaming is disabled)
flushIntervalDuration30sInterval between event flush batches
flushBatchSizeint100Maximum number of events per flush batch
initTimeoutDuration10sMaximum time to wait for initialization

Configuration is applied through the client builder rather than constructed directly.

Obtain a client via the static factory (recommended):

FeatureflipClient client = FeatureflipClient.get("sdk-dev-abc123");

Or with configuration via the builder:

FeatureflipClient client = FeatureflipClient.get("sdk-dev-abc123",
FeatureFlagConfig.builder()
.baseUrl("https://eval.featureflip.io")
.streaming(true)
.initTimeout(Duration.ofSeconds(30))
.build());

Both forms perform the initial flag fetch and start background streaming or polling. If the initial fetch fails, initialization continues in the background.

Blocks until the client has loaded flag data. Throws FeatureFlagInitializationException if the timeout expires.

client.waitForInitialization();
boolean ready = client.isInitialized();

Returns true once the client has successfully loaded flag data.

MethodParameterDescription
baseUrl()StringSet the Evaluation API base URL
connectTimeout()DurationSet the connection timeout
readTimeout()DurationSet the read timeout
streaming()booleanEnable or disable SSE streaming
pollInterval()DurationSet the polling interval
flushInterval()DurationSet the event flush interval
flushBatchSize()intSet the maximum events per batch
initTimeout()DurationSet the initialization timeout
build()Build the client and start initialization

All evaluation methods accept a flag key, an evaluation context, and a default value. If the flag is not found or an error occurs, the default value is returned.

EvaluationContext context = EvaluationContext.builder("u-123").build();
boolean enabled = client.boolVariation("dark-mode", context, false);

Signature:

boolean boolVariation(String key, EvaluationContext context, boolean defaultValue)
String plan = client.stringVariation("pricing-tier", context, "free");

Signature:

String stringVariation(String key, EvaluationContext context, String defaultValue)
int limit = client.intVariation("rate-limit", context, 100);

Signature:

int intVariation(String key, EvaluationContext context, int defaultValue)
double ratio = client.doubleVariation("rollout-ratio", context, 0.5);

Signature:

double doubleVariation(String key, EvaluationContext context, double defaultValue)

Deserializes a JSON flag value to the specified class using Jackson:

BannerConfig banner = client.jsonVariation(
"banner-config",
context,
new BannerConfig("Welcome", "#000"),
BannerConfig.class
);

Signature:

<T> T jsonVariation(String key, EvaluationContext context, T defaultValue, Class<T> type)

Detail methods return an EvaluationDetail<T> object with the value and metadata about the decision.

EvaluationDetail<Boolean> detail = client.boolVariationDetail("dark-mode", context, false);
System.out.println(detail.getValue()); // true
System.out.println(detail.getReason()); // RULE_MATCH
System.out.println(detail.getRuleId()); // "rule-abc"
System.out.println(detail.getErrorMessage()); // null
EvaluationDetail<String> detail = client.stringVariationDetail("pricing-tier", context, "free");
EvaluationDetail<Integer> detail = client.intVariationDetail("rate-limit", context, 100);
EvaluationDetail<Double> detail = client.doubleVariationDetail("rollout-ratio", context, 0.5);
MethodReturn typeDescription
getValue()TThe evaluated flag value
getReason()EvaluationReasonWhy this value was returned
getRuleId()StringThe ID of the matched targeting rule (may be null)
getErrorMessage()StringError message if evaluation failed (may be null)
ValueDescription
RULE_MATCHA targeting rule matched the context
FALLTHROUGHNo rules matched; the fallthrough variation was served
FLAG_DISABLEDThe flag is disabled; the off variation was served
FLAG_NOT_FOUNDThe flag key does not exist
ERRORAn error occurred during evaluation

The EvaluationContext class uses a builder pattern with a required user ID. Custom attributes feed into targeting rules, and the user ID anchors percentage rollouts so each user gets a stable bucket assignment:

EvaluationContext context = EvaluationContext.builder("u-123")
.set("email", "[email protected]")
.set("country", "US")
.set("plan", "pro")
.build();
MethodParameterDescription
builder()String userIdCreate a builder with the required user ID
set()String key, Object valueAdd a custom attribute (case-insensitive keys)
build()Build the immutable context
String userId = context.getUserId(); // "u-123"
Object plan = context.getAttribute("plan"); // "pro"

getAttribute() checks custom attributes first, then falls back to built-in properties (userId, user_id).

Records a custom analytics event.

Map<String, Object> metadata = Map.of(
"plan", "pro",
"amount", 29.99
);
client.track("purchase-completed", context, metadata);

Signature:

void track(String eventName, EvaluationContext context, Map<String, Object> metadata)

Events are batched and flushed automatically based on flushInterval and flushBatchSize.

Forces all buffered events to be sent to the server immediately.

client.flush();

The client implements AutoCloseable. Use try-with-resources for automatic cleanup:

try (FeatureflipClient client = FeatureflipClient.get("sdk-key")) {
boolean enabled = client.boolVariation("dark-mode", context, false);
}
// client.close() is called automatically

Stops SSE streaming or polling, flushes remaining events, and shuts down the executor service (with a 5-second graceful timeout). Safe to call multiple times.

client.close();

Creates a test client with fixed flag values. No network calls are made and no background threads are started.

FeatureflipClient client = FeatureflipClient.forTesting(Map.of(
"dark-mode", true,
"pricing-tier", "enterprise",
"rate-limit", 500
));
EvaluationContext ctx = EvaluationContext.builder("test-user").build();
client.boolVariation("dark-mode", ctx, false); // true
client.stringVariation("pricing-tier", ctx, "free"); // "enterprise"

Signature:

static FeatureflipClient forTesting(Map<String, Object> values)

The test client is fully initialized immediately. Flag values are returned regardless of the evaluation context. The values map is copied and immutable.

Thrown by waitForInitialization() when the client cannot load flag data within the configured timeout. Also thrown if initialization is interrupted.

try {
client.waitForInitialization();
} catch (FeatureFlagInitializationException e) {
// Handle initialization failure
log.error("Failed to initialize: {}", e.getMessage());
}

The SDK uses the following runtime dependencies:

LibraryPurpose
OkHttp 4.xHTTP client and SSE
Jackson 2.xJSON serialization/deserialization
SLF4J 2.xLogging facade