Java SDK for Featureflip - evaluate feature flags locally with near-zero latency.
implementation 'io.featureflip:featureflip-java:2.15.0'<dependency> <groupId>io.featureflip</groupId> <artifactId>featureflip-java</artifactId> <version>2.15.0</version> </dependency>
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()(orbuilder().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.
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());
To let a new process start from its last-known configuration when the Evaluation API is unreachable, opt in with .snapshotStore(new FileSnapshotStore("/var/lib/featureflip/snapshot.json")) and optionally .maxSnapshotAgeSeconds(...) (default 86400). client.startupInfo() reports whether the data came from the API, a snapshot, or nowhere. Implement SnapshotStore (get() and put(text)) to keep the snapshot somewhere shared such as Redis. See the Java SDK docs.
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.
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);
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);
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.
// 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();
Featureflip counts each event name, per environment. The metadata you pass is transmitted with the event but is not stored, and event counts are not surfaced in the app or API.
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
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)
- Local evaluation - Near-zero latency after initialization
- Real-time updates - SSE streaming with automatic polling fallback
- Change notifications -
onUpdatelisteners 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
- Java 11+
Apache-2.0