Java SDK
This supports Java applications using Java version 1.8 and higher.
Installation
Gradle
To install in a Gradle project, add Jitpack to your repositories, and then add the dependency with the latest version to your project's dependencies.
- Groovy
- Kotlin
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.growthbook:growthbook-sdk-java:0.5.0'
}
repositories {
maven {
setUrl("https://jitpack.io")
}
}
dependencies {
implementation("com.github.growthbook:growthbook-sdk-java:0.5.0")
}
Maven
To install in a Maven project, add Jitpack to your repositories:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
Next, add the dependency with the latest version to your project's dependencies:
<dependency>
<groupId>com.github.growthbook</groupId>
<artifactId>growthbook-sdk-java</artifactId>
<version>0.5.0</version>
</dependency>
Usage
There are two main approaches to using the GrowthBook Java SDK:
- Enhanced Client (Recommended) - For better performance with multi-context support
- Traditional per-request approach - Create a new context and SDK instance per request
Enhanced Client (Recommended)
Available starting in version 0.9.0
For improved performance and better resource management, especially in web applications, use the enhanced client pattern. This approach allows you to reuse a single client instance across multiple requests while providing different user contexts for each evaluation while calling the feature methods like isOn()
.
This GrowthBookClient
instance is decoupled from the GBContext
, creates a singleton featureRepository based on your refreshStrategy and uses the latest features at the time of
evaluation, all managed internally.
Basic Enhanced Client Usage
// build options to configure your Growthbook instance
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.build();
// Create growthbook instance using the options you need
GrowthBookClient gb = new GrowthBookClient(options);
// call the init method to load features
gb.initialize();
gb.isOn("featureKey", UserContext.builder()
.attributesJson("{\"id\" : \"123\"}").build()
);
Using the Enhanced Client with Different User Contexts
// build options to configure your Growthbook instance
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.build();
// Create growthbook instance using the options you need
GrowthBookClient gb = new GrowthBookClient(options);
// call the init method to load features
gb.initialize();
// Use the same client instance with different user contexts
// User 1 context
UserContext user1Context = UserContext.builder()
.attributesJson("{\"id\":\"user_123\",\"country\":\"US\",\"premium\":true}")
.build();
boolean showNewFeature = gb.isOn("new-homepage", user1Context);
String buttonColor = gb.getFeatureValue("button-color", "blue", user1Context);
// User 2 context - same client, different context
UserContext user2Context = UserContext.builder()
.attributesJson("{\"id\":\"user_456\",\"country\":\"CA\",\"premium\":false}")
.build();
boolean showFeatureForUser2 = gb.isOn("new-homepage", user2Context);
String colorForUser2 = gb.getFeatureValue("button-color", "blue", user2Context);
Thread Safety Note
While the enhanced client is designed for concurrent use, full concurrency support is still being refined. For high-concurrency applications, consider:
- Using connection pooling for database-backed sticky bucket services
- Implementing proper synchronization for custom tracking callbacks
- Testing thoroughly under expected load conditions
Traditional Usage
For new projects, we recommend using the Enhanced Client instead for better performance.
There are 2 steps to initializing the traditional GrowthBook SDK:
- Create a GrowthBook context
GBContext
with the features JSON and the user attributes - Create the
GrowthBook
SDK class with the context
GrowthBook context
The GrowthBook context GBContext
can be created either by implementing the builder class, available at GBContext.builder()
, or by using the GBContext
constructor.
Field name | Type | Description |
---|---|---|
attributesJson | String | The user attributes JSON. See Attributes. |
featuresJson | String | The features JSON served by the GrowthBook API (or equivalent). See Features. |
enabled | Boolean | Whether to enable the functionality of the SDK (default: true ) |
isQaMode | Boolean | Whether the SDK is in QA mode. Not for production use. If true, random assignment is disabled and only explicitly forced variations are used (default: false ) |
url | String | The URL of the current page. Useful when evaluating features and experiments based on the page URL. |
forcedVariationsMap | Map<String, Integer> | Force specific experiments to always assign a specific variation (used for QA) |
trackingCallback | TrackingCallback | A callback that will be invoked with every experiment evaluation where the user is included in the experiment. See TrackingCallback. To subscribe to all evaluated events regardless of whether the user is in the experiment, see Subscribing to experiment runs. |
featureUsageCallback | FeatureUsageCallback | A callback that will be invoked every time a feature is viewed. See FeatureUsageCallback |
Using the GBContext builder
The builder is the easiest to use way to construct a GBContext
, allowing you to provide as many or few arguments as you'd like. All fields mentioned above are available via the builder.
- Java
- Kotlin
// Fetch feature definitions from the GrowthBook API
// We recommend adding a caching layer in production
// Get your endpoint in the Environments tab -> SDK Endpoints: https://app.growthbook.io/environments
URI featuresEndpoint = new URI("https://cdn.growthbook.io/api/features/<environment_key>");
HttpRequest request = HttpRequest.newBuilder().uri(featuresEndpoint).GET().build();
HttpResponse<String> response = HttpClient.newBuilder().build()
.send(request, HttpResponse.BodyHandlers.ofString());
String featuresJson = new JSONObject(response.body()).get("features").toString();
// JSON serializable user attributes
String userAttributesJson = user.toJson();
// Initialize the GrowthBook SDK with the GBContext
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributesJson)
.build();
GrowthBook growthBook = new GrowthBook(context);
// Fetch feature definitions from the GrowthBook API
// We recommend adding a caching layer in production
// Get your endpoint in the Environments tab -> SDK Endpoints: https://app.growthbook.io/environments
val featuresEndpoint = URI.create("https://cdn.growthbook.io/api/features/<environment_key>")
val request = HttpRequest.newBuilder().uri(featuresEndpoint).GET().build();
val response = HttpClient.newBuilder().build()
.send(request, HttpResponse.BodyHandlers.ofString());
val featuresJson = JSONObject(response.body()).get("features").toString()
// JSON serializable user attributes
val userAttributes = """
{
"id": "user-abc123",
"country": "canada"
}
""".trimIndent()
// Initialize the GrowthBook SDK with the GBContext
val context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributes)
.build()
val growthBook = GrowthBook(context)
The above example uses java.net.http.HttpClient
which, depending on your web framework, may not be the best option, in which case it is recommended to use a networking library more suitable for your implementation.
Using the GBContext constructor
You can also use GBContext
constructor if you prefer, which will require you to pass all arguments explicitly.
- Java
- Kotlin
// Fetch feature definitions from the GrowthBook API
// We recommend adding a caching layer in production
// Get your endpoint in the Environments tab -> SDK Endpoints: https://app.growthbook.io/environments
URI featuresEndpoint = new URI("https://cdn.growthbook.io/api/features/<environment_key>");
HttpRequest request = HttpRequest.newBuilder().uri(featuresEndpoint).GET().build();
HttpResponse<String> response = HttpClient.newBuilder().build()
.send(request, HttpResponse.BodyHandlers.ofString());
String featuresJson = new JSONObject(response.body()).get("features").toString();
// JSON serializable user attributes
String userAttributesJson = user.toJson();
boolean isEnabled = true;
boolean isQaMode = false;
String url = null;
Map<String, Integer> forcedVariations = new HashMap<>();
TrackingCallback trackingCallback = new TrackingCallback() {
@Override
public <ValueType> void onTrack(Experiment<ValueType> experiment, ExperimentResult<ValueType> experimentResult) {
// TODO: Something after it's been tracked
}
};
// Initialize the GrowthBook SDK with the GBContext
GBContext context = new GBContext(
userAttributesJson,
featuresJson,
isEnabled,
isQaMode,
url,
forcedVariations,
trackingCallback
);
GrowthBook growthBook = new GrowthBook(context);
// Fetch feature definitions from the GrowthBook API
// We recommend adding a caching layer in production
// Get your endpoint in the Environments tab -> SDK Endpoints: https://app.growthbook.io/environments
val featuresEndpoint = URI.create("https://cdn.growthbook.io/api/features/<environment_key>")
val request = HttpRequest.newBuilder().uri(featuresEndpoint).GET().build();
val response = HttpClient.newBuilder().build()
.send(request, HttpResponse.BodyHandlers.ofString());
val featuresJson = JSONObject(response.body()).get("features").toString()
// JSON serializable user attributes
val userAttributes = """
{
"id": "user-abc123",
"country": "canada"
}
""".trimIndent()
val isEnabled = true
val isQaMode = false
val url: String? = null
val forcedVariations = mapOf<String, Int>()
val trackingCallback: TrackingCallback = object : TrackingCallback {
override fun <ValueType : Any?> onTrack(
experiment: Experiment<ValueType>?,
experimentResult: ExperimentResult<ValueType>?
) {
// TODO: Something after it's been tracked
}
}
// Initialize the GrowthBook SDK with the GBContext
val context = GBContext(
userAttributes,
featuresJson,
isEnabled,
isQaMode,
url,
forcedVariations,
trackingCallback
)
val growthBook = GrowthBook(context)
For complete examples, see the Examples section below.
Features
The features JSON is equivalent to the features
property that is returned from the SDK Connection endpoint.
- You can read more about features here
- You can see an example features JSON here
Attributes
Attributes are a JSON string. You can specify attributes about the current user and request. Here's an example:
- Java
- Kotlin
String userAttributes = "{\"country\": \"canada\", \"id\": \"user-abc123\"}";
val userAttributes = """
{
"id": "user-abc123",
"country": "canada"
}
""".trimIndent()
If you need to set or update attributes asynchronously, you can do so with Context#attributesJson
or GrowthBook#setAttributes
. This will completely overwrite the attributes object with whatever you pass in. Also, be aware that changing attributes may change the assigned feature values. This can be disorienting to users if not handled carefully.
Secure Attributes
When secure attribute hashing is enabled, all targeting conditions in the SDK payload referencing attributes with datatype secureString
or secureString[]
will be anonymized via SHA-256 hashing. This allows you to safely target users based on sensitive attributes. You must enable this feature in your SDK Connection for it to take effect.
If your SDK Connection has secure attribute hashing enabled, you will need to manually hash any secureString
or secureString[]
attributes that you pass into the GrowthBook SDK.
To hash an attribute, use a cryptographic library with SHA-256 support, and compute the SHA-256 hashed value of your attribute plus your organization's secure attribute salt.
- Enhanced Client
- Traditional Client
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
// Your secure attribute salt (set in Organization Settings)
String salt = "f09jq3fij";
// Create and initialize the enhanced client once
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.build();
GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
// Helper method to hash secure attributes
private String hashSecureAttribute(String value, String salt) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest((salt + value).getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 not available", e);
}
}
// Hash secure attributes for each user evaluation
String userEmail = user.getEmail();
String hashedEmail = hashSecureAttribute(userEmail, salt);
List<String> userTags = user.getTags();
List<String> hashedTags = userTags.stream()
.map(tag -> hashSecureAttribute(tag, salt))
.collect(Collectors.toList());
// Create user context with hashed attributes
UserContext userContext = UserContext.builder()
.attributesJson(String.format(
"{\"id\":\"%s\",\"loggedIn\":true,\"email\":\"%s\",\"tags\":%s}",
user.getId(),
hashedEmail,
new Gson().toJson(hashedTags)
))
.build();
// Use the enhanced client with hashed attributes
boolean hasFeature = gb.isOn("premium-feature", userContext);
String theme = gb.getFeatureValue("theme", "light", userContext);
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
// Your secure attribute salt (set in Organization Settings)
String salt = "f09jq3fij";
// Hashing a secureString attribute
String userEmail = user.getEmail();
String hashedEmail = "";
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest((salt + userEmail).getBytes(StandardCharsets.UTF_8));
hashedEmail = Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
// Handle exception
}
// Hashing a secureString[] attribute
List<String> userTags = user.getTags();
List<String> hashedTags = new ArrayList<>();
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (String tag : userTags) {
byte[] hash = digest.digest((salt + tag).getBytes(StandardCharsets.UTF_8));
hashedTags.add(Base64.getEncoder().encodeToString(hash));
}
} catch (NoSuchAlgorithmException e) {
// Handle exception
}
// Create attributes JSON with hashed values
String attributesJson = String.format(
"{\"id\":\"%s\",\"loggedIn\":true,\"email\":\"%s\",\"tags\":%s}",
user.getId(),
hashedEmail,
Arrays.toString(hashedTags.toArray())
);
// Use the hashed attributes when creating your GrowthBook instance
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(attributesJson)
.build();
GrowthBook growthBook = new GrowthBook(context);
Using Features
Every feature has a "value" which is assigned to a user. This value can be any JSON data type. If a feature doesn't exist, the value will be null
.
There are 4 main methods for evaluating features.
Method | Return type | Description |
---|---|---|
isOn(String) | Boolean | Returns true if the value is a truthy value |
isOff(String) | Boolean | Returns true if the value is a falsy value |
getFeatureValue(String) | generic T (nullable) | Returns the value cast to the generic type. Type is inferred based on the defaultValue argument provided. |
evalFeature(String) | FeatureResult<T> | Returns a feature result with a value of generic type T . The value type needs to be specified in the generic parameter. |
- Enhanced Client
- Traditional Client
// Create enhanced client
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.build();
GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
// Create user context for evaluations
UserContext userContext = UserContext.builder()
.attributesJson("{\"id\":\"user_123\",\"premium\":true}")
.build();
// Boolean feature checks
if (gb.isOn("dark_mode", userContext)) {
// value is truthy
}
if (gb.isOff("dark_mode", userContext)) {
// value is falsy
}
// Get feature values with defaults
Float donutPrice = gb.getFeatureValue("donut_price", 5.0f, userContext);
String theme = gb.getFeatureValue("theme", "light", userContext);
Integer maxItems = gb.getFeatureValue("max_items", 10, userContext);
// Get detailed feature result
FeatureResult<Float> priceResult = gb.evalFeature("donut_price", userContext);
Float price = priceResult.getValue();
String source = priceResult.getSource().toString();
// Create traditional client
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson("{\"id\":\"user_123\",\"premium\":true}")
.build();
GrowthBook growthBook = new GrowthBook(context);
// Boolean feature checks
if (growthBook.isOn("dark_mode")) {
// value is truthy
}
if (growthBook.isOff("dark_mode")) {
// value is falsy
}
// Get feature values with defaults
Float donutPrice = growthBook.getFeatureValue("donut_price", 5.0f);
String theme = growthBook.getFeatureValue("theme", "light");
Integer maxItems = growthBook.getFeatureValue("max_items", 10);
// Get detailed feature result
FeatureResult<Float> priceResult = growthBook.<Float>evalFeature("donut_price");
Float price = priceResult.getValue();
String source = priceResult.getSource().toString();
isOn() / isOff()
These methods return a boolean for truthy and falsy values.
Only the following values are considered to be "falsy":
null
false
""
0
Everything else is considered "truthy", including empty arrays and objects.
If the value is "truthy", then isOn()
will return true and isOff()
will return false. If the value is "falsy", then the opposite values will be returned.
getFeatureValue(featureKey, defaultValue)
This method has a variety of overloads to help with casting values to primitive and complex types.
In short, the type of the defaultValue
argument will determine the return type of the function.
Return type | Method | Additional Info |
---|---|---|
Boolean | getFeatureValue(String featureKey, Boolean defaultValue) | |
Double | getFeatureValue(String featureKey, Double defaultValue) | |
Float | getFeatureValue(String featureKey, Float defaultValue) | |
Integer | getFeatureValue(String featureKey, Integer defaultValue) | |
String | getFeatureValue(String featureKey, String defaultValue) | |
<ValueType> ValueType | getFeatureValue(String featureKey, ValueType defaultValue, Class<ValueType> gsonDeserializableClass) | Internally, the SDK uses Gson. You can pass any class that does not require a custom deserializer. |
Object | getFeatureValue(String featureKey, Object defaultValue) | Use this method if you need to cast a complex object that uses a custom deserializer, or if you use a different JSON serialization library than Gson, and cast the type yourself. |
See the Java Docs for more information.
See the unit tests for example implementations including type casting for all above-mentioned methods.
evalFeature(String)
The evalFeature
method returns a FeatureResult<T>
object with more info about why the feature was assigned to the user. The T
type corresponds to the value type of the feature. In the above example, T
is Float
.
FeatureResult<T>
It has the following getters.
Method | Return type | Description |
---|---|---|
getValue() | generic T (nullable) | The evaluated value of the feature |
getSource() | enum FeatureResultSource (nullable) | The reason/source for the evaluated feature value. |
getRuleId() | String (nullable) | ID of the rule that was used to assign the value to the user. |
getExperiment() | Experiment<T> (nullable) | The experiment details, available only if the feature evaluates due to an experiment. |
getExperimentResult() | ExperimentResult<T> (nullable) | The experiment result details, available only if the feature evaluates due to an experiment. |
As expected in Kotlin, you can access these getters using property accessors.
Inline Experiments
Instead of declaring all features up-front in the context and referencing them by IDs in your code, you can also just run an experiment directly. This is done with the growthbook.run(Experiment<T>)
method.
- Enhanced Client
- Traditional Client
// Create enhanced client
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.build();
GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
// Create user context
UserContext userContext = UserContext.builder()
.attributesJson("{\"id\":\"user_123\",\"employee\":true}")
.build();
// Create and run inline experiment
Experiment<Float> donutPriceExperiment = Experiment
.<Float>builder()
.key("donut-price-test")
.variations(Arrays.asList(2.50f, 3.00f, 3.50f))
.weights(Arrays.asList(0.33f, 0.33f, 0.34f))
.conditionJson("{\"employee\": true}")
.build();
ExperimentResult<Float> result = gb.run(donutPriceExperiment, userContext);
Float donutPrice = result.getValue();
// Check if user was included in experiment
if (result.getInExperiment()) {
System.out.println("User is in experiment, variation: " + result.getVariationId());
System.out.println("Donut price: $" + donutPrice);
} else {
System.out.println("User not in experiment");
}
// String experiment example
Experiment<String> buttonColorExperiment = Experiment
.<String>builder()
.key("button-color")
.variations(Arrays.asList("blue", "red", "green"))
.build();
ExperimentResult<String> colorResult = gb.run(buttonColorExperiment, userContext);
String buttonColor = colorResult.getValue();
// Create traditional client
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson("{\"id\":\"user_123\",\"employee\":true}")
.build();
GrowthBook growthBook = new GrowthBook(context);
// Create and run inline experiment
Experiment<Float> donutPriceExperiment = Experiment
.<Float>builder()
.key("donut-price-test")
.variations(Arrays.asList(2.50f, 3.00f, 3.50f))
.weights(Arrays.asList(0.33f, 0.33f, 0.34f))
.conditionJson("{\"employee\": true}")
.build();
ExperimentResult<Float> result = growthBook.run(donutPriceExperiment);
Float donutPrice = result.getValue();
// Check if user was included in experiment
if (result.getInExperiment()) {
System.out.println("User is in experiment, variation: " + result.getVariationId());
System.out.println("Donut price: $" + donutPrice);
} else {
System.out.println("User not in experiment");
}
// String experiment example
Experiment<String> buttonColorExperiment = Experiment
.<String>builder()
.key("button-color")
.variations(Arrays.asList("blue", "red", "green"))
.build();
ExperimentResult<String> colorResult = growthBook.run(buttonColorExperiment);
String buttonColor = colorResult.getValue();
Inline experiment return value ExperimentResult
An ExperimentResult<T>
is returned where T
is the generic value type for the experiment.
There's also a number of methods available.
Method | Return type | Description |
---|---|---|
getValue() | generic T (nullable) | The evaluated value of the feature |
getVariationId() | Integer (nullable) | Index of the variation used, if applicable |
getInExperiment() | Boolean | If the user was in the experiment. This will be false if the user was excluded from being part of the experiment for any reason (e.g. failed targeting conditions). |
getHashAttribute() | String | User attribute used for hashing, defaulting to id if not set. |
getHashValue() | String (nullable) | The hash value used for evaluating the experiment, if applicable. |
getFeatureId() | String | The feature key/ID |
getHashUsed() | Boolean | If a hash was used to evaluate the experiment. This flag will only be true if the user was randomly assigned a variation. If the user was forced into a specific variation instead, this flag will be false. |
As expected in Kotlin, you can access these getters using property accessors.
Tracking feature usage and experiment impressions
This section covers how to track and monitor feature usage and experiment impressions in your application. There are several types of callbacks and events you can subscribe to:
- Tracking Callback - Called when a user is included in an experiment
- Feature Usage Callback - Called every time a feature is evaluated
- Experiment Run Callback - Called for every experiment evaluation (regardless of inclusion)
Tracking Callback
Any time an experiment is run to determine the value of a feature, we may call this callback so you can record the assigned value in your event tracking or analytics system of choice.
The tracking callback is only called when the user is in the experiment. If they are not in the experiment, this will not be called.
- Enhanced Client
- Traditional Client
// Create tracking callback
TrackingCallback trackingCallback = new TrackingCallback() {
@Override
public <ValueType> void onTrack(
Experiment<ValueType> experiment,
ExperimentResult<ValueType> experimentResult
) {
// Send to your analytics system
analytics.track("experiment_viewed", Map.of(
"experiment_id", experiment.getKey(),
"variation_id", experimentResult.getVariationId(),
"user_id", experimentResult.getHashAttribute()
));
}
};
// Create enhanced client with tracking
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.trackingCallback(trackingCallback)
.build();
GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
// Use features - tracking callback will be called automatically when user is in experiment
UserContext userContext = UserContext.builder()
.attributesJson("{\"id\":\"user_123\",\"employee\":true}")
.build();
boolean newFeature = gb.isOn("new-checkout-flow", userContext);
// Create tracking callback
TrackingCallback trackingCallback = new TrackingCallback() {
@Override
public <ValueType> void onTrack(
Experiment<ValueType> experiment,
ExperimentResult<ValueType> experimentResult
) {
// Send to your analytics system
System.out.println("Experiment tracked: " + experiment.getKey());
System.out.println("Variation: " + experimentResult.getVariationId());
}
};
// Create context with tracking callback
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributesJson)
.trackingCallback(trackingCallback)
.build();
GrowthBook growthBook = new GrowthBook(context);
// Use features - tracking callback will be called when user is in experiment
boolean newFeature = growthBook.isOn("new-checkout-flow");
Feature Usage Callback
Any time a feature is evaluated (regardless of experiment participation), this callback is called with the feature key and result.
- Enhanced Client
- Traditional Client
// Create feature usage callback
FeatureUsageCallback featureUsageCallback = new FeatureUsageCallback() {
@Override
public <ValueType> void onFeatureUsage(
String featureKey,
FeatureResult<ValueType> featureResult
) {
// Log feature usage
logger.info("Feature '{}' evaluated with value: {}",
featureKey, featureResult.getValue());
// Send to analytics
analytics.track("feature_used", Map.of(
"feature_key", featureKey,
"feature_value", featureResult.getValue(),
"source", featureResult.getSource().toString()
));
}
};
// Create enhanced client with feature usage tracking
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.featureUsageCallback(featureUsageCallback)
.build();
GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
// Feature usage callback will be called for every feature evaluation
UserContext userContext = UserContext.builder()
.attributesJson("{\"id\":\"user_123\"}")
.build();
String theme = gb.getFeatureValue("theme", "light", userContext);
boolean darkMode = gb.isOn("dark_mode", userContext);
// Create feature usage callback
FeatureUsageCallback featureUsageCallback = new FeatureUsageCallback() {
@Override
public <ValueType> void onFeatureUsage(
String featureKey,
FeatureResult<ValueType> featureResult
) {
// Log feature usage
System.out.println("Feature used: " + featureKey +
" = " + featureResult.getValue());
}
};
// Create context with feature usage callback
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributesJson)
.featureUsageCallback(featureUsageCallback)
.build();
GrowthBook growthBook = new GrowthBook(context);
// Feature usage callback will be called for every feature evaluation
String theme = growthBook.getFeatureValue("theme", "light");
boolean darkMode = growthBook.isOn("dark_mode");
Experiment Run Callback
You can subscribe to experiment run evaluations using the ExperimentRunCallback
. This callback is called for every experiment evaluation, regardless of whether the user is included in the experiment.
- Enhanced Client
- Traditional Client
// Create experiment run callback
ExperimentRunCallback experimentRunCallback = new ExperimentRunCallback() {
@Override
public <ValueType> void onExperimentRun(
Experiment<ValueType> experiment,
ExperimentResult<ValueType> experimentResult
) {
// Track all experiment evaluations (included and excluded users)
analytics.track("experiment_evaluated", Map.of(
"experiment_id", experiment.getKey(),
"user_in_experiment", experimentResult.getInExperiment(),
"variation_id", experimentResult.getVariationId(),
"hash_used", experimentResult.getHashUsed()
));
}
};
// Enhanced client doesn't directly support ExperimentRunCallback
// Use inline experiments with the callback
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.build();
GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
// Subscribe to experiment runs (you can subscribe multiple callbacks)
gb.subscribe(experimentRunCallback);
// Run inline experiments - callback will be triggered
UserContext userContext = UserContext.builder()
.attributesJson("{\"id\":\"user_123\",\"employee\":true}")
.build();
Experiment<String> buttonColorExperiment = Experiment
.<String>builder()
.key("button-color")
.variations(Arrays.asList("blue", "red", "green"))
.build();
ExperimentResult<String> result = gb.run(buttonColorExperiment, userContext);
// Create experiment run callback
ExperimentRunCallback experimentRunCallback = new ExperimentRunCallback() {
@Override
public <ValueType> void onExperimentRun(
Experiment<ValueType> experiment,
ExperimentResult<ValueType> experimentResult
) {
// Track all experiment evaluations
System.out.println("Experiment evaluated: " + experiment.getKey());
System.out.println("User in experiment: " + experimentResult.getInExperiment());
}
};
// Create GrowthBook instance
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributesJson)
.build();
GrowthBook growthBook = new GrowthBook(context);
// Subscribe to experiment runs (you can subscribe multiple callbacks)
growthBook.subscribe(experimentRunCallback);
// Run inline experiments - callback will be triggered
Experiment<String> buttonColorExperiment = Experiment
.<String>builder()
.key("button-color")
.variations(Arrays.asList("blue", "red", "green"))
.build();
ExperimentResult<String> result = growthBook.run(buttonColorExperiment);
Multiple Callback Subscriptions
You can subscribe to multiple experiment run callbacks:
- Enhanced Client
- Traditional Client
// Multiple callbacks for different purposes
ExperimentRunCallback analyticsCallback = (experiment, result) -> {
// Send to analytics
analytics.track("experiment_run", Map.of(
"experiment", experiment.getKey(),
"included", result.getInExperiment()
));
};
ExperimentRunCallback loggingCallback = (experiment, result) -> {
// Log for debugging
logger.debug("Experiment {} evaluated for user {}",
experiment.getKey(), result.getHashAttribute());
};
// Subscribe multiple callbacks
gb.subscribe(analyticsCallback);
gb.subscribe(loggingCallback);
// Multiple callbacks for different purposes
ExperimentRunCallback analyticsCallback = (experiment, result) -> {
// Send to analytics
System.out.println("Analytics: " + experiment.getKey());
};
ExperimentRunCallback loggingCallback = (experiment, result) -> {
// Log for debugging
System.out.println("Debug: " + experiment.getKey());
};
// Subscribe multiple callbacks
growthBook.subscribe(analyticsCallback);
growthBook.subscribe(loggingCallback);
Working with Encrypted features
As of version 0.3.0, the Java SDK supports decrypting encrypted features. You can learn more about SDK Connection Endpoint Encryption.
The main difference is you create a GBContext
by passing an encryption key (.encryptionKey()
when using the builder) and using the encrypted payload as the features JSON (.featuresJson()
for the builder).
- Java
- Kotlin
// Fetch feature definitions from the GrowthBook API
// We recommend adding a caching layer in production
// Get your endpoint in the Environments tab -> SDK Endpoints: https://app.growthbook.io/environments
URI featuresEndpoint = new URI("https://cdn.growthbook.io/api/features/<environment_key>");
HttpRequest request = HttpRequest.newBuilder().uri(featuresEndpoint).GET().build();
HttpResponse<String> response = HttpClient.newBuilder().build()
.send(request, HttpResponse.BodyHandlers.ofString());
String encryptedFeaturesJson = new JSONObject(response.body()).get("encryptedFeatures").toString();
// JSON serializable user attributes
String userAttributesJson = user.toJson();
// You can store your encryption key as an environment variable rather than hardcoding in plain text in your codebase
String encryptionKey = "<key-for-decrypting>";
// Initialize the GrowthBook SDK with the GBContext and the encryption key
GBContext context = GBContext
.builder()
.featuresJson(encryptedFeaturesJson)
.encryptionKey(encryptionKey)
.attributesJson(userAttributesJson)
.build();
GrowthBook growthBook = new GrowthBook(context);
// Fetch feature definitions from the GrowthBook API
// We recommend adding a caching layer in production
// Get your endpoint in the Environments tab -> SDK Endpoints: https://app.growthbook.io/environments
val featuresEndpoint = URI.create("https://cdn.growthbook.io/api/features/<environment_key>")
val request = HttpRequest.newBuilder().uri(featuresEndpoint).GET().build();
val response = HttpClient.newBuilder().build()
.send(request, HttpResponse.BodyHandlers.ofString());
val encryptedFeaturesJson = JSONObject(response.body()).get("encryptedFeatures").toString()
// JSON serializable user attributes
val userAttributes = """
{
"id": "user-abc123",
"country": "canada"
}
""".trimIndent()
// You can store your encryption key as an environment variable rather than hardcoding in plain text in your codebase
val encryptionKey = "<key-for-decrypting>";
// Initialize the GrowthBook SDK with the GBContext and the encryption key
val context = GBContext
.builder()
.featuresJson(encryptedFeaturesJson)
.encryptionKey(encryptionKey)
.attributesJson(userAttributes)
.build()
val growthBook = GrowthBook(context)
Fetching, Cacheing, and Refreshing features with GBFeaturesRepository
As of version 0.4.0, the Java SDK provides an optional GBFeaturesRepository
class which will manage networking for you in the following ways:
- Fetching features from the SDK endpoint when
initialize()
is called - Decrypting encrypted features when provided with the client key, e.g.
.builder().encryptionKey(clientKey)
- Cacheing features (in-memory)
- Refreshing features
If you wish to manage fetching, refreshing, and cacheing features on your own, you can choose to not implement this class.
This class should be implemented as a singleton class as it includes caching and refreshing functionality.
If you have more than one SDK endpoint you'd like to implement, you can extend the GBFeaturesRepository
class with your own class to make it easier to work with dependency injection frameworks. Each of these instances should be singletons.
Fetching the features
You will need to create a singleton instance of the GBFeaturesRepository
class either by implementing its .builder()
or by using its constructor.
Then, you would call myGbFeaturesRepositoryInstance.initialize()
in order to make the initial (blocking) request to fetch the features. Then, you would call myGbFeaturesRepositoryInstance.getFeaturesJson()
and provided that to the GBContext
initialization.
- Java
- Kotlin
GBFeaturesRepository featuresRepository = GBFeaturesRepository
.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("<environment_key>") // replace with your client key
.encryptionKey("<client-key-for-decrypting>") // optional, nullable
.refreshStrategy(FeatureRefreshStrategy.SERVER_SENT_EVENTS) // optional; options: STALE_WHILE_REVALIDATE, SERVER_SENT_EVENTS (default: STALE_WHILE_REVALIDATE)
.build();
// Optional callback for getting updates when features are refreshed
featuresRepository.onFeaturesRefresh(new FeatureRefreshCallback() {
@Override
public void onRefresh(String featuresJson) {
System.out.println("Features have been refreshed");
System.out.println(featuresJson);
}
});
try {
featuresRepository.initialize();
} catch (FeatureFetchException e) {
// TODO: handle the exception
e.printStackTrace();
}
// Initialize the GrowthBook SDK with the GBContext and features
GBContext context = GBContext
.builder()
.featuresJson(featuresRepository.getFeaturesJson())
.attributesJson(userAttributesJson)
.build();
GrowthBook growthBook = new GrowthBook(context);
val featuresRepository = GBFeaturesRepository(
"https://cdn.growthbook.io",
"<environment_key>",
"<client-key-for-decrypting>", // optional, nullable
30,
).apply {
// Optional callback for getting updates when features are refreshed
onFeaturesRefresh {
println("Features have been refreshed \n $it")
}
}
// Fetch the features
try {
featuresRepository.initialize()
} catch (e: FeatureFetchException) {
// TODO: handle the exception
e.printStackTrace()
}
// Initialize the GrowthBook SDK with the GBContext and features
val context = GBContext
.builder()
.featuresJson(featuresRepository.featuresJson)
.attributesJson(userAttributes)
.build()
val growthBook = GrowthBook(context)
For more references, see the Examples below.
Cacheing and refreshing behavior
As of version 0.9.0, there are 2 refresh strategies available.
Stale While Revalidate
This is the default strategy but can be explicitly stated by passing FeatureRefreshStrategy.STALE_WHILE_REVALIDATE
as the refresh strategy option to the GBFeaturesRepository
builder or constructor.
The GBFeaturesRepository
will automatically refresh the features when the features become stale. Features are considered stale every 60 seconds. This amount is configurable with the ttlSeconds
option.
When you fetch features and they are considered stale, the stale features are returned from the getFeaturesJson()
method and a network call to refresh the features is enqueued asynchronously. When that request succeeds, the features are updated with the newest features, and the next call to getFeaturesJson()
will return the refreshed features.
Server-Sent Events
This is a new strategy that can be enabled by passing FeatureRefreshStrategy.SERVER_SENT_EVENTS
as the refresh strategy option to the GBFeaturesRepository
builder or constructor.
If you're using GrowthBook Cloud , this is ready for you to use. If you are self-hosting, you will need to set up the GrowthBook Proxy to enable it.
Overriding Feature Values
The Java SDK allows you to override feature values and experiments using the URL.
Force Experiment Variations
You can force an experiment variation by passing the experiment key and variation index as query parameters in the URL you set on the GBContext
. For example, if you add ?my-experiment-id=2
to the URL, users will be forced into the variation at index 2 in the variations list when evaluating the experiment with key my-experiment-id
.
Force Feature Values via the URL
You can force a value for a feature by passing the key, prefixed by gb~
, and the URI-encoded value in the URL's query parameters. You must also set allowUrlOverrides
to true when building your GBContext
in order to enable this feature as it is not enabled by default.
GBContext context = GBContext
.builder()
.url("http://localhost:8080/url-feature-force?gb~dark_mode=true&gb~donut_price=3.33&gb~banner_text=Hello%2C%20everyone!%20I%20hope%20you%20are%20all%20doing%20well!")
.allowUrlOverrides(true)
.build();
The above code sample sets the following:
dark_mode
:true
banner_text
:"Hello, everyone! I hope you are all doing well!"
donut_price
:3.33
Supported types
Type | Value |
---|---|
String | A URI-encoded string value |
Float | A float value, e.g. 3.33 |
Double | A double value, e.g. 3.33 |
Integer | An integer value, e.g. 1337 |
Boolean | A boolean value, e.g. true or false . You can also represent boolean values with on/off state as on or off or binary values 1 or 0 . |
JSON as Gson -deserializable | A class that can be deserialized using Gson and that does not have any dependencies on custom type adapters. |
JSON as String | A URI-encoded string value that should be a valid JSON string that you can deserialize with your own JSON deserialization implementation. |
The value passed in the URL is cast at runtime based on the generic type argument passed in when evaluating the feature. This means that when you call <ValueType>getFeatureValue()
, what you pass into the URL must successfully cast as ValueType
otherwise the value in the URL will be ignored.
All keys must be prefixed with gb~
.
Using with Proguard and R8
Many Android projects use code-shrinking and obfuscation tools like Proguard and R8 in production.
If you are experiencing unexpected feature evaluation results with your release Android builds that do not occur in your debug builds, it's most likely related to this.
You will need to add the following to your proguard-rules.pro
file to ensure that all of the GrowthBook SDK classes are kept so that your features are evaluated properly in projects that use Proguard and R8:
# Growthbook Java SDK classes
-keep class growthbook.sdk.java.** { *; }
Code Examples
Web Framework Integration with Spring RestController
@RestController
public class FeatureController {
// Singleton enhanced client instance
private final GrowthBookClient growthBookClient;
@Autowired
public FeatureController() {
// build options to configure your Growthbook instance
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.build();
// Create growthbook instance using the options you need
this.growthBookClient = new GrowthBookClient(options);
// Initialize features once
this.growthBookClient.initialize();
}
@GetMapping("/api/features")
public ResponseEntity<Map<String, Object>> getFeatures(HttpServletRequest request) {
// Create user context per request
UserContext userContext = createUserContext(request);
// Evaluate features using shared client
Map<String, Object> features = new HashMap<>();
features.put("darkMode", growthBookClient.isOn("dark-mode", userContext));
features.put("maxItems", growthBookClient.getFeatureValue("max-items", 10, userContext));
features.put("newLayout", growthBookClient.isOn("new-layout", userContext));
return ResponseEntity.ok(features);
}
@GetMapping("/api/user/{userId}/features")
public ResponseEntity<Map<String, Object>> getUserFeatures(@PathVariable String userId) {
// Create user-specific context
UserContext userContext = UserContext.builder()
.attributesJson(String.format("{\"id\":\"%s\",\"loggedIn\":true}", userId))
.build();
// Evaluate features for specific user
Map<String, Object> features = new HashMap<>();
features.put("premiumFeature", growthBookClient.isOn("premium-feature", userContext));
features.put("theme", growthBookClient.getFeatureValue("theme", "light", userContext));
return ResponseEntity.ok(features);
}
private UserContext createUserContext(HttpServletRequest request) {
// Extract user attributes from request
String userId = getUserIdFromSession(request);
String userAgent = request.getHeader("User-Agent");
String country = getCountryFromRequest(request);
// Build attributes JSON
String attributesJson = String.format(
"{\"id\":\"%s\",\"userAgent\":\"%s\",\"country\":\"%s\",\"loggedIn\":%s}",
userId, userAgent, country, userId != null
);
return UserContext.builder()
.attributesJson(attributesJson)
.build();
}
private String getUserIdFromSession(HttpServletRequest request) {
// Your implementation to extract user ID from session/JWT/etc
return request.getSession().getAttribute("userId") != null
? request.getSession().getAttribute("userId").toString()
: null;
}
private String getCountryFromRequest(HttpServletRequest request) {
// Your implementation to determine user's country
return request.getHeader("CF-IPCountry"); // Example using Cloudflare header
}
}
Additional Examples
Further Reading
Supported Features
FeaturesAll versions
ExperimentationAll versions
Prerequisites≥ v0.9.3
Sticky Bucketing≥ v0.9.3
Streaming≥ v0.9.0
SemVer Targeting≥ v0.7.0
v2 Hashing≥ v0.6.0
Encrypted Features≥ v0.3.0