Java SDK
This supports Java applications using Java version 1.8 and higher.
The GrowthBook Java SDK is a brand new feature. If you experience any issues, let us know either on Slack or create an issue.
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.2.2'
}
repositories {
maven {
setUrl("https://jitpack.io")
}
}
dependencies {
implementation("com.github.growthbook:growthbook-sdk-java:0.2.2")
}
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.2.2</version>
</dependency>
Usage
There are 2 steps to initializing the 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 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. |
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 Endpoint for the specified environment. You can find your endpoint on the Environments → SDK Endpoints page .
- 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.
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. If you'd like to subscribe to all evaluations, regardless of experiment result, see Subscribing to experiment runs.
- Java
- Kotlin
TrackingCallback trackingCallback = new TrackingCallback() {
@Override
public <ValueType> void onTrack(
Experiment<ValueType> experiment,
ExperimentResult<ValueType> experimentResult
) {
// TODO: Something after it's been tracked
}
};
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributesJson)
.trackingCallback(trackingCallback)
.build();
val trackingCallback: TrackingCallback = object : TrackingCallback {
override fun <ValueType : Any?> onTrack(
experiment: Experiment<ValueType>?,
experimentResult: ExperimentResult<ValueType>?
) {
// TODO: Something after it's been tracked
}
}
val context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributes)
.trackingCallback(trackingCallback)
.build()
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. |
- Java
- Kotlin
if (growthBook.isOn("dark_mode")) {
// value is truthy
}
if (growthBook.isOff("dark_mode")) {
// value is falsy
}
Float featureValue = growthBook.getFeatureValue("donut_price", 5.0f);
FeatureResult<Float> featureResult = growthBook.<Float>evalFeature("donut_price");
if (growthBook.isOn("dark_mode")) {
// value is truthy
}
if (growthBook.isOff("dark_mode")) {
// value is falsy
}
val featureValue = growthBook.getFeatureValue("donut_price", 5.0f)
val featureResult = growthBook.evalFeature<Float>("donut_price")
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.
- Java
- Kotlin
Experiment<Float> donutPriceExperiment = Experiment
.<Float>builder()
.conditionJson("{\"employee\": true}")
.build();
ExperimentResult<Float> donutPriceExperimentResult = growthBook.run(donutPriceExperiment);
Float donutPrice = donutPriceExperimentResult.getValue();
val donutPriceExperiment = Experiment
.builder<Float>()
.conditionJson("""
{
"employee": true
}
""".trimIndent())
.build()
val donutPriceExperimentResult = growthBook.run(donutPriceExperiment)
val donutPrice = donutPriceExperimentResult.value
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.
Subscribing to experiment runs with the ExperimentRunCallback
You can subscribe to experiment run evaluations using the ExperimentRunCallback
.
- Java
- Kotlin
String featuresJson = featuresRepository.getFeaturesJson();
String userAttributesJson = user.toJson();
ExperimentRunCallback experimentRunCallback = new ExperimentRunCallback() {
@Override
public void onRun(ExperimentResult experimentResult) {
// TODO: something with the experiment result
}
};
GBContext context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributesJson)
.build();
GrowthBook growthBook = new GrowthBook(context);
growthBook.subscribe(experimentRunCallback);
val featuresJson = featuresRepository.getFeaturesJson()
val userAttributes = """
{
"id": "user-abc123",
"country": "canada"
}
""".trimIndent()
val experimentRunCallback = object : ExperimentRunCallback {
override fun onRun(experimentResult: ExperimentResult<*>?) {
TODO("Something with the experiment result")
}
}
val context = GBContext
.builder()
.featuresJson(featuresJson)
.attributesJson(userAttributes)
.build()
val growthBook = GrowthBook(context)
growthBook.subscribe(experimentRunCallback)
Every time an experiment is evaluated when calling growthBook.run(Experiment)
, the callbacks will be called.
You can subscribe with as many callbacks as you like:
- Java
- Kotlin
GrowthBook growthBook = new GrowthBook(context);
growthBook.subscribe(experimentRunCallback1);
growthBook.subscribe(experimentRunCallback2);
growthBook.subscribe(experimentRunCallback3);
In Kotlin, you can also use the following syntax, if desired:
val growthBook = GrowthBook(context).apply {
subscribe(experimentRunCallback1)
subscribe(experimentRunCallback2)
subscribe(experimentRunCallback3)
}
The experiment run callback is called for every experiment run, regardless of experiment result. If you would like to subscribe only to evaluations where the user falls into an experiment, see TrackingCallback.
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()
.endpoint("https://cdn.growthbook.io/api/features/<environment_key>")
.encryptionKey("<client-key-for-decrypting>") // optional, nullable
.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/api/features/<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 behaviour
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.