Welcome to the Java SDK implementation for FeatureHub.io - Open source Feature flags management, A/B testing and remote configuration platform.
This is the version 3 of the SDKs for OKHTTP, Jersey 2, and Jersey 3. It is a departure from the Version 2 libraries because all possible clients are collected together (SSO, Passive Rest, Active Rest).
The minimum version of Java supported is Java 11. Some libraries support only Java 17+ because of their dependencies.
It is generally recommended that you should use the OKHttp version (io.featurehub.sdk:java-client-okhttp:3+) of the library by preference unless you
are already using a Jersey 2 or Jersey 3 stack.
|
Note
|
We are using Gradle standard for referring to version ranges, so 3+ means
[3,4) in Maven.
|
You can look at the examples to see what we have done for each stack we have examples for (Quarkus, Spring 7, Jersey 2 and Jersey 3) as a starter.
If you are starting a new project and do not already have an HTTP or JSON library in your stack, the simplest option is the convenience artifact that bundles everything you need in a single dependency:
<dependency>
<groupId>io.featurehub.sdk</groupId>
<artifactId>featurehub-okhttp3-jackson2</artifactId>
<version>[3,4)</version>
</dependency>This pulls in java-client-okhttp, composite-okhttp, common-jacksonv2, and a SLF4j logging implementation.
It is the recommended starting point for most users.
The base libraries do not include dependencies on Jackson (which they need) or a logging framework (which they also need). A basic OKHttp client will usually require:
-
io.featurehub.sdk:java-client-okhttp:3+- the basic OKHttp library + shared SDK libraries -
all of the necessary okhttp components - you can find this in
io.featurehub.sdk.composites:composite-okhttplocated in link:support/composite-okhttp/pom.xml -
a jackson adapter depending on which version of Jackson (2 or 3) you are using - so
io.featurehub.sdk.common:common-jacksonv2:1+orio.featurehub.sdk.common:common-jacksonv3:1+. Jackson 3 /common-jacksonv3requires Java 17+ and is published underv17-and-above/support/. -
an SLF4j implementation - we use
io.featurehub.sdk.composites:sdk-composite-logging:1+( link:support/composite-logging/pom.xml ) in this SDK but it is only the API that is a required dependency and we expect you to provide one.
Jersey 2 and Jersey 3 have equivalent dependencies in io.featurehub.sdk:java-client-jersey2:3+ and io.featurehub.sdk:java-client-jersey3:3+ respectively. We also do not include the foundation libraries for these
in our dependencies as we assume you have them in your stack already, which is why you are choosing those
implementations.
We generally recommend using OKHttp if you do not already have Jersey.
It is expected that you will first create a FeatureHub config instance.
import io.featurehub.client.EdgeFeatureHubConfig;
// typically you would get these from environment variables
String edgeUrl = "http://localhost:8085/";
String apiKey = "71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE";
FeatureHubConfig fhConfig = new EdgeFeatureHubConfig(edgeUrl, apiKey);There are 3 ways to request for feature updates via this SDK:
-
Server Sent Events - these are near realtime events, so the events get pushed to you. The connection to the server lasts usually 1-3 minutes (it can be longer depending on how your admin has it configured), and the SDK will then disconnect and reconnect again, ensuring it has received all feature updates in the meantime. This is typically the mode used by Java server based projects. You specify this in code by choosing
fhConfig.streaming().init(). -
Passive REST - This is where a polling interval is set. There is an initial request for feature state, but until a feature is evaluated and that polling interval has been exceeded, the client will not ask for a fresh set of features or check if any have changed. This is a good choice where there is a low incidence of feature updates, but is usually used on mobile devices (like Android) where you don’t want continuous polling if the user isn’t doing anything. You specify this in code by choosing
fhConfig.restPassive().init(). -
Active REST - This is where the client will make a request for updated state every X seconds regardless if anyone is using it. You specify this in code by choosing
fhConfig.restActive().init().
If you are using Server Evaluated keys, you do not want to call init(). You need to create your first
ClientContext (see below) and call build() - which will trigger a connection, passing all the requisite
data to the FeatureHub server for evaluation.
Its always good to look at examples on how to do what you want. We have examples for:
-
Spring Boot 7 (
v17-and-above/examples/todo-java-springboot) - streaming client, Java 17+ -
Quarkus (
v17-and-above/examples/todo-java-quarkus) - native image support, Java 17+ -
Jersey 2 (
examples/todo-java-jersey2) - configurable for OpenTelemetry/Segment, all connection modes -
Jersey 3 (
examples/todo-java-jersey3) - same as Jersey 2 but with Jakarta REST -
Batch (
examples/batch) - batch processing / short-lived process pattern -
Migration check (
examples/migration-check) - dynamically swaps REST then SSE at runtime -
Cucumber BDD (
examples/todo-cuke-java) - integration testing with FeatureHub’s test API
Every FeatureHub SDK works the same basic way - it needs the URL of your FeatureHub server, and an API key.
You give those two things to the FeatureHubConfig (in Java, its the EdgeFeatureHubConfig), then specify
your client type (see above, SSE, Active or Passive REST) and then initialize.
The SDK takes the responsibility of getting the features from the server, keeping a local copy of them in memory, and then responding to your requests for feature evaluations.
Feature evaluations are always done within the scope of a ClientContext - which is just a bag of attributes
(a map) you want to keep track of about the current user, request, etc, so that you can use targeting in your feature
evaluation (called strategies). Where those strategies are evaluated depends on the type of key you are using.
If you use a client evaluated key - as is normal for Java apps - all of the necessary data for decision making comes to the Java app and it makes decisions there. This is most ideally for any kind of situation where there will ever be more than one instance of a ClientContext - like a web server for instance.
If you use a server evaluated key, all those attributes get sent to the server and it evaluates the feature values and returns them to you.
If you have confidential information in your features and your client is not confidential, you should use a server evaluated key, otherwise you should generally use a client evaluated key.
The Readiness enum has three states:
| State | Meaning |
|---|---|
|
The SDK has not yet received its first set of features from the server. Feature evaluations at this point return default values. |
|
The SDK has received at least one full feature set and is ready for use. It stays |
|
A terminal failure has occurred (e.g. invalid API key, authentication error). The SDK will stop retrying. This requires operator intervention (fix the key or server configuration) and a restart. |
Once your SDK has the list of features it will go into Ready, and won’t drop back out again even if it loses
the connection or ability to talk to your server.
We recommend adding FeatureHub to your heartbeat or liveness check:
@RequestMapping("/liveness")
public String liveness() {
if (featureHubConfig.getReadiness() == Readiness.Ready) {
return "yes";
}
log.warn("FeatureHub connection not yet available, reporting not live.");
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE);
}This will prevent most services like Application Load Balancers or Kubernetes from routing traffic to your server before it has connected to the feature service and is ready.
Instead of polling getReadiness(), you can register a callback that fires whenever the SDK transitions
between states. Both FeatureHubConfig and FeatureRepository expose addReadinessListener:
RepositoryEventHandler sub = fhConfig.addReadinessListener(readiness -> {
if (readiness == Readiness.Ready) {
log.info("FeatureHub is ready");
} else if (readiness == Readiness.Failed) {
log.error("FeatureHub entered a terminal failure state");
}
});
// when you no longer need the listener:
sub.cancel();The listener is called immediately with the current state when registered, and then again on every transition.
The returned RepositoryEventHandler.cancel() removes the listener to prevent memory leaks.
The next thing you would normally do is to ensure that the ClientContext is ready and set up for downstream
systems to get a hold of and use. In Java this is normally done by using a filter and providing some
kind of request level scope - a Request Level injectable object.
In our examples, we simply put the Authorization header into the UserKey of the context, allowing you to just pass the name of the user to keep it simple. You can see each platform’s example to see how this is done in alternative ways.
@Configuration
public class UserConfiguration {
@Bean
@Scope("request")
ClientContext createClient(FeatureHubConfig fhConfig, HttpServletRequest request) {
ClientContext fhClient = fhConfig.newContext();
if (request.getHeader("Authorization") != null) {
// you would always authenticate some other way, this is just an example
fhClient.userKey(request.getHeader("Authorization"));
}
return fhClient;
}
}
@RestController
public class HelloResource {
...
@RequestMapping("/")
public String index() {
ClientContext fhClient = clientProvider.get();
return "Hello World " + fhClient.feature("SUBMIT_COLOR_BUTTON").getString();
}
}These examples show us how we can wire the FeatureHub functionality into our system in two different cases, the standard CDI (with extensions) way that Quarkus (and to a degree Jersey) works, and the way that Spring/SpringBoot works.
Server side evaluation
In the server side evaluation (e.g. an Android Mobile app or a Batch application), the context is created once as you evaluate one user per client. This config is likely loaded into resources that are baked into your Mobile image and once you load them, you can progress from there.
You should not use Server Sent Events for Mobile as they attempt to keep the radio on and will drain battery. For Mobile we recommend restPassive() as the mode chosen for this reason. It will only poll if the poll
timeout has occurred and a user is evaluating a feature.
As such, it is recommended that you create your ClientContext as early as sensible and build it. This will trigger
a poll to the server and it will get the feature statuses and you will be ready to go. Each time you need an update,
you can simply .build() your context again and it will force a poll.
ClientContext fhClient = fhConfig.newContext().build().get();
For frameworks where you cannot easily inject a request-scoped ClientContext (e.g. plain JAX-RS filters
or legacy codebases), ThreadLocalContext provides a thread-local alternative.
Set the config once at startup:
ThreadLocalContext.setConfig(fhConfig);Then anywhere on the same thread — in a filter, a resource method, a service — retrieve the context:
ClientContext ctx = ThreadLocalContext.getContext(); // or .context()When the request completes, clean up to avoid context leaking into thread-pool reuse:
// e.g. in a servlet filter's finally block
ThreadLocalContext.close();close() calls ctx.close() and removes the entry from the thread-local, so the thread is clean for the
next request.
All ClientContext and FeatureRepository methods that accept a feature name are overloaded to accept either
a plain String or an instance of the Feature interface. Implementing Feature on an enum gives you
compile-time safety and easy IDE refactoring:
public enum MyFeatures implements Feature {
SUBMIT_BUTTON_COLOR,
NEW_CHECKOUT_FLOW,
MAX_ITEMS_PER_PAGE
}
// no magic strings
boolean enabled = ctx.isEnabled(MyFeatures.NEW_CHECKOUT_FLOW);
String color = ctx.feature(MyFeatures.SUBMIT_BUTTON_COLOR).getString();The Feature interface requires only a name() method, which enums provide automatically.
ClientContext.feature(key) and FeatureRepository.feature(key) return a FeatureState<?> with the
following accessors:
| Method | Returns |
|---|---|
|
String value, or |
|
|
|
|
|
Raw JSON string, or |
|
Deserialised object of the given type using the configured Jackson mapper, or |
|
|
|
|
|
|
|
|
|
|
Register a FeatureListener on any FeatureState to be notified when its value changes:
ctx.feature("SUBMIT_BUTTON_COLOR").addListener(featureState -> {
log.info("SUBMIT_BUTTON_COLOR changed to {}", featureState.getString());
refreshUi();
});|
Note
|
Do not add a listener to a ClientContext-scoped FeatureState in server-evaluated mode when many
contexts are created per request — each context holds its listener list and this will cause a memory leak.
In server-evaluated mode, register the listener on the repository-level feature instead:
fhConfig.getRepository().feature("KEY").addListener(…).
|
For repository-wide change notifications, FeatureRepository exposes two streams, both returning a
RepositoryEventHandler whose cancel() removes the subscription:
// fires for every individual feature that changes value
RepositoryEventHandler sub = fhConfig.getRepository()
.registerFeatureUpdateAvailable(fs -> log.info("{} = {}", fs.getKey(), fs.getString()));
// fires once whenever a fresh full state arrives (SSE reconnect or REST poll)
RepositoryEventHandler sub2 = fhConfig.getRepository()
.registerNewFeatureStateAvailable(repo -> log.info("fresh feature set received"));
// to unsubscribe:
sub.cancel();Starting from version 1.1.0 FeatureHub supports server side evaluation of complex rollout strategies that are applied to individual feature values in a specific environment. This includes support of preset rules, e.g. per user key, country, device type, platform type as well as percentage splits rules and custom rules that you can create according to your application needs.
For more details on rollout strategies, targeting rules and feature experiments see the core documentation.
We are actively working on supporting client side evaluation of strategies in the future releases as this scales better when you have 10000+ consumers.
There are several preset strategies rules we track specifically: user key, country, device and platform. However, if those do not satisfy your requirements you also have an ability to attach a custom rule. Custom rules can be created as following types: string, number, boolean, date, date-time, semantic-version, ip-address
FeatureHub SDK will match your users according to those rules, so you need to provide attributes to match on in the SDK:
Sending preset attributes:
Provide the following attribute to support userKey rule:
fhClient.userKey("ideally-unique-id");to support country rule:
fhClient.country(StrategyAttributeCountryName.NewZealand);to support device rule:
fhClient.device(StrategyAttributeDeviceName.Browser);to support platform rule:
fhClient.platform(StrategyAttributePlatformName.Android);to support semantic-version rule:
fhClient.version("1.2.0");or if you are using multiple rules, you can combine attributes as follows:
fhClient.userKey("ideally-unique-id")
.country(StrategyAttributeCountryName.NewZealand)
.device(StrategyAttributeDeviceName.Browser)
.platform(StrategyAttributePlatformName.Android)
.version("1.2.0");If you are using Server Evaluated API Keys then you should always run .build() which will execute a background
poll. If you wish to ensure the next line of code has the updated statuses, wait for the future to complete with .get()
ClientContext fhClient = fhConfig.newContext().userKey("user@mailinator.com").build().get();You do not have to do the build().get() (but you can) for client evaluated keys as the context is mutable and changes are immediate. As the context is evaluated locally, it will always be ready the very next line of code.
Sending custom attributes:
To add a custom key/value pair, use attr(key, value)
fhClient.attr("first-language", "russian");Or with array of values (only applicable to custom rules):
fhClient.attrs("languages", Arrays.asList("thai", "english", "german"));You can also use fhClient.clear() to empty your context.
Remember, for Server Evaluated Keys you must always call .build() to trigger a request to update the feature values
based on the context changes.
Coding for percentage splits:
For percentage rollout you are only required to provide the userKey or sessionKey.
fhClient.userKey("ideally-unique-id");or
fhClient.sessionKey("session-id");For more details on percentage splits and feature experiments see Percentage Split Rule.
Both methods accept a Map<String, List<String>> but behave differently:
| Method | Behaviour |
|---|---|
|
Replaces the entire custom-attribute map. Any attributes previously set (excluding preset fields like
|
|
Merges the supplied map into the existing attributes. Existing keys are overwritten; keys not present in the new map are preserved. |
// single value — returns the first element if multi-valued, or null
String lang = fhClient.getAttr("first-language");
String lang = fhClient.getAttr("first-language", "english"); // with default
// multi-value — returns List<String>, or null if absent
List<String> langs = fhClient.getAttrs("languages");
List<String> langs = fhClient.getAttrs("languages", "english"); // default wrapped in a listNew in this version is also considerable control over server connection connectivity. The values can be set using environment variables or system properties.
-
featurehub.edge.server-connect-timeout-ms- defaults to 5000 -
featurehub.edge.server-sse-read-timeout-ms- defaults to 1800000 - 3m (180 seconds), should be higher if the server is configured for longer by default -
featurehub.edge.server-rest-read-timeout-ms- defaults to 150000 - 15s - should be very fast for a REST request as its a connect, read and disconnect process -
featurehub.edge.server-disconnect-retry-ms- defaults to 0 - immediately try and reconnect if disconnected -
featurehub.edge.server-by-reconnect-ms- defaults to 0 - if the SSE server disconnects using a "bye", how long to wait before reconnecting -
featurehub.edge.backoff-multiplier- defaults to 10 -
featurehub.edge.maximum-backoff-ms- defaults to 30000
This will not be affected by API keys not existing, that will stop connectivity completely. Also, if you are using the SaaS version and you have exceeded your maximum connects that you have specified, it will also stop after the first success.
When the SDK changes connection state it uses the EdgeConnectionState enum. This is relevant when implementing
a custom EdgeService or diagnosing connectivity problems from logs:
| State | Applies to | Meaning |
|---|---|---|
|
SSE + REST |
Connection established and data received. |
|
SSE only |
Normal server-initiated disconnect after the configured timeout. The SDK reconnects after a short delay. |
|
SSE + REST |
Disconnected before a "bye" was received. The SDK backs off and retries. |
|
SSE + REST |
Timed out waiting for data. Retryable with backoff. |
|
SSE + REST |
Low-level failure (e.g. unknown host, refused connection). Retried with exponential backoff. |
|
SSE + REST |
Terminal — the API key is not recognised by the server. The repository transitions to |
|
SSE + REST |
Terminal failure (e.g. 401/403 from Edge). The SDK stops retrying. |
Feature Interceptors are the ability to intercept the request for a feature. They only operate in imperative state. For an overview check out the Documentation on them.
We currently support one built-in feature interceptor:
-
io.featurehub.client.interceptor.SystemPropertyValueInterceptor- source reads system properties; if a property namedfeaturehub.feature.FEATURE_NAMEis set andfeaturehub.features.allow-override=trueis also set, the property value overrides the server value. This is useful for a developer who wants to enable a feature flag locally without affecting others.
Implement FeatureValueInterceptor and register it via FeatureHubConfig:
public class EnvVarInterceptor implements FeatureValueInterceptor {
@Override
public ValueMatch getValue(String key) {
String val = System.getenv("FEATURE_" + key.toUpperCase());
return new ValueMatch(val != null, val);
}
}
// false = respect the lock; true = override even locked features
fhConfig.registerValueInterceptor(false, new EnvVarInterceptor());Usage Adapters (new in version 3 of the Core SDK) let you hook into every feature evaluation event. Whenever a
feature value is read from a ClientContext, the SDK fires a UsageEvent carrying the feature key, its evaluated
value, and a snapshot of all context attributes (user key, country, device, custom attrs, etc.). Registered plugins
receive that event and can forward it to any analytics or observability backend.
The core wiring is:
FeatureRepository ──registerUsageStream──▶ UsageAdapter ──▶ UsagePlugin(s)UsageAdapter is created automatically by EdgeFeatureHubConfig and holds a list of UsagePlugin instances.
Each UsagePlugin.send(UsageEvent) is called synchronously for every evaluation.
FeatureHubConfig fhConfig = new EdgeFeatureHubConfig(edgeUrl, apiKey);
// register one or more plugins before calling init()
fhConfig.registerUsagePlugin(new OpenTelemetryUsagePlugin());
fhConfig.registerUsagePlugin(new SegmentUsagePlugin(segmentWriteKey));
fhConfig.streaming().init();registerUsagePlugin returns FeatureHubConfig so calls can be chained.
The classes involved in a usage event are:
| Class / Interface | Purpose |
|---|---|
|
Base class. Carries a nullable |
|
Interface that adds |
|
The standard per-evaluation event. Extends |
|
Value object attached to |
|
A bulk snapshot of all features currently held in the repository, serialised as |
|
Extends |
Extend UsagePlugin and implement send(UsageEvent event):
import io.featurehub.client.usage.UsageEvent;
import io.featurehub.client.usage.UsageEventName;
import io.featurehub.client.usage.UsagePlugin;
public class MyAnalyticsPlugin extends UsagePlugin {
@Override
public void send(UsageEvent event) {
if (!(event instanceof UsageEventName)) return;
String name = ((UsageEventName) event).getEventName();
String user = event.getUserKey(); // may be null
Map<String, ?> props = event.toMap(); // feature + context attrs
// forward to your analytics backend here
myClient.track(name, user, props);
}
}You can also add default properties that are merged into every event for this plugin:
MyAnalyticsPlugin plugin = new MyAnalyticsPlugin();
plugin.getDefaultEventParams().put("app_version", "2.1.0");
plugin.getDefaultEventParams().put("environment", "production");
fhConfig.registerUsagePlugin(plugin);If you need access to the raw usage stream without a plugin (e.g. to batch or filter events), you can register a callback directly on the repository:
RepositoryEventHandler sub = fhConfig.getRepository()
.registerUsageStream(event -> myHandler(event));
// later, to unsubscribe:
sub.cancel();Attaches each feature evaluation to the active OpenTelemetry Span — either as span attributes (default,
no extra cost) or as span events (enables multiple evaluations per span at the cost of additional data).
// default prefix "featurehub.", attributes mode
fhConfig.registerUsagePlugin(new OpenTelemetryUsagePlugin());
// custom prefix
fhConfig.registerUsagePlugin(new OpenTelemetryUsagePlugin("fh."));Set FEATUREHUB_OTEL_SPAN_AS_EVENTS=true to switch to events mode. The plugin is safe to include even when
OpenTelemetry is not active — it checks for a valid span before doing anything.
See OpenTelemetry adapter README for details.
Integrates with Twilio Segment to push feature evaluation events and/or enrich all outgoing Segment messages with the current user’s feature state.
Tracking individual feature evaluations
// simplest — reads key from env var FEATUREHUB_USAGE_SEGMENT_WRITE_KEY
// or system property featurehub.usage.segment-write-key
fhConfig.registerUsagePlugin(new SegmentUsagePlugin());
// explicit write key
fhConfig.registerUsagePlugin(new SegmentUsagePlugin(segmentWriteKey));
// bring your own OkHttpClient (proxies, timeouts, etc.) and optional message transformers
fhConfig.registerUsagePlugin(new SegmentUsagePlugin(segmentWriteKey, okHttpClient, transformers));
// bring your own pre-built Analytics object
fhConfig.registerUsagePlugin(new SegmentUsagePlugin(myAnalytics));Augmenting all Segment messages with context — use SegmentMessageTransformer to add the current user’s
feature values and context attributes to every outgoing Segment message (Track, Identify, etc.):
SegmentMessageTransformer transformer = new SegmentMessageTransformer(
new Message.Type[]{Message.Type.TRACK, Message.Type.IDENTIFY},
() -> ThreadLocalContext.getContext(), // how to retrieve the current ClientContext
false, // useAnonymousUser
true // setUserOnMessage
);
Analytics analytics = Analytics.builder(segmentWriteKey)
.messageTransformer(transformer)
.build();
fhConfig.registerUsagePlugin(new SegmentUsagePlugin(analytics));SegmentMessageTransformer calls ClientContext.fillUsageCollection(UsageFeaturesCollectionContext) to
collect all evaluated features and context attributes, then sets them as Segment message context.
See Segment adapter README for details.
FeatureHub provides a Test API endpoint that lets tests update feature values at runtime without restarting the server. This is used to drive CI/CD integration tests and BDD scenarios.
Obtain a TestApi from the config (each transport implementation registers one via ServiceLoader):
TestApi testApi = fhConfig.newTestApi();
// change a feature — uses the first/only configured API key
TestApiResult result = testApi.setFeatureState(
"MY_FEATURE",
new FeatureStateUpdate().lock(false).value(true)
);
if (result.isChanged()) {
// value was updated
} else if (result.isNotChanged()) {
// already at that value — no action needed
} else if (result.isNotPossible()) {
// feature is locked — unlock it first
} else if (result.isNotPermitted()) {
// this API key does not have permission to change features
} else if (result.isNonExistant()) {
// feature key or environment not found
}
testApi.close();If you have multiple environments or API keys configured, you can target a specific one:
testApi.setFeatureState(specificApiKey, "MY_FEATURE", update);TestApiResult response codes at a glance:
| Method | HTTP code | Meaning |
|---|---|---|
|
2xx |
Request was accepted. |
|
201 |
Feature value was updated. |
|
200 or 202 |
Feature was already in the requested state. |
|
400 |
Request was malformed (e.g. missing data). |
|
403 |
The API key does not have permission to change this feature. |
|
404 |
Service key, environment, or feature key not found. |
|
412 |
Operation not possible in current state (e.g. value change without unlocking first). |
See examples/todo-cuke-java for a complete BDD test harness using the Test API.
The SDK gives convenience methods for usage, but you can make it do almost anything you like.
If you want to specify and deliberately configure it, you can use:
fhConfig.setEdgeService(() -> new EdgeProvider(...));Where EdgeProvider is the name of your class that knows how to connect to the Edge and pull feature details.
There is an example in examples/migration-check which does a REST connection initially, and when it has
the features, it will update the repository, allowing the features to be evaluated correctly, but stop the
REST connection and swap to SSE.
By default EdgeFeatureHubConfig creates a ClientFeatureRepository with a single background thread.
You can supply your own with a larger thread pool or a completely custom ExecutorService:
// larger thread pool (min 3 threads, max is Math.max(n, 10))
FeatureRepository repo = new ClientFeatureRepository(4);
// bring your own executor
FeatureRepository repo = new ClientFeatureRepository(myExecutorService);
fhConfig.setRepository(repo);Run build_only.sh (or look at it and run the same commands) to install it
on your own system. Once installed you should be able to load it into your IDE.
The Java 11 libraries are kept separate from the Java 17+ versions as they build separately in CI. You can load the link:pom.xml and link:v17-and-above/pom.xml into your IDE separately.
The SDK consists of the following published artifacts:
-
io.featurehub.sdk:java-client-api(core/client-java-api) — OpenAPI-generated SSE/REST model classes. Tracks the main FeatureHub repository; changes are always backwards compatible. -
io.featurehub.sdk:java-client-core(core/client-java-core) — All domain logic: in-memory feature repository,ClientContext, rollout strategy evaluation (ApplyFeature), usage adapters, interceptors, and listener infrastructure. Does not connect to the outside world. -
io.featurehub.sdk:java-client-okhttp(client-implementations/java-client-okhttp) — OKHttp-basedEdgeService. The recommended transport. -
io.featurehub.sdk:java-client-jersey2(client-implementations/java-client-jersey2) — JAX-RS 2.x transport. -
io.featurehub.sdk:java-client-jersey3(client-implementations/java-client-jersey3) — Jakarta REST 3.x transport. -
io.featurehub.sdk:featurehub-okhttp3-jackson2(support/featurehub-okhttp3-jackson2) — Convenience bundle: OKHttp + Jackson 2 + composites. Recommended for new projects. -
io.featurehub.sdk.common:common-jacksonv2(support/common-jacksonv2) — Jackson 2.x JSON adapter. Required unless usingfeaturehub-okhttp3-jackson2. -
io.featurehub.sdk.common:common-jacksonv3(v17-and-above/support/common-jacksonv3) — Jackson 3.x JSON adapter. Requires Java 17+; built separately underv17-and-above/. -
io.featurehub.sdk.composites:composite-okhttp/jersey2/jersey3/logging(support/) — Composite POMs that centralise compatible dependency versions. Import into<dependencyManagement>to inherit versions without pulling in the SDK itself.
The v17-and-above/ directory is a separate Maven reactor built with a JDK 17+ toolchain:
cd v17-and-above && mvn installIt contains:
-
support/common-jacksonv3— Jackson 3 adapter (incompatible with Java 11) -
examples/todo-java-springboot— Spring Boot 7 example -
examples/todo-java-quarkus— Quarkus native-image example