- Java version 1.8 or later
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.
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.growthbook:growthbook-sdk-java:0.5.0'
}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>We are proposing two way of initializing SDK:
GrowthBookClient lets you share the same instance for all requests with an ability to accept the user attributes
while calling the feature methods like isOn(). This GrowthBookClient instance is decoupled from the GBContext,
owns a feature repository based on your refresh strategy and uses the latest features at the time of evaluation,
all managed internally.
// 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);
// Optional metadata-only listener for successful and failed feature refreshes
FeatureRefreshSubscription refreshSubscription = gb.subscribeFeatureRefreshListener(event -> {
if (event.isSuccessful()) {
System.out.println("Refresh source: " + event.getSource());
System.out.println("Features changed: " + event.isFeaturesChanged());
System.out.println("Loaded from cache: " + event.isLoadedFromCache());
System.out.println("Refresh duration: " + event.getDurationMillis() + " ms");
System.out.println("Active features: " + event.getActiveFeatureCount());
} else {
System.out.println("Feature refresh failed: " + event.getError());
}
});
// call the init method to load features
gb.initialize();
gb.isOn("featureKey", UserContext.builder()
.attributesJson("{\"id\" : \"123\"}").build()
);
// Stop receiving refresh events when no longer needed
refreshSubscription.close();Feature refresh listeners are dispatched off the refresh thread on a dedicated daemon thread owned by
the client. Supply your own executor with
Options.builder().featureRefreshListenerExecutor(executor).build() to control the threading yourself.
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 listener for getting metadata when features are refreshed
FeatureRefreshListener refreshListener = event -> {
if (event.isSuccessful()) {
System.out.println("Features have been refreshed");
System.out.println("Features changed: " + event.isFeaturesChanged());
} else {
System.out.println("Features refreshed with error");
System.out.println(event.getError());
}
};
featuresRepository.addFeatureRefreshListener(refreshListener);
try {
featuresRepository.initialize();
} catch (FeatureFetchException e) {
// TODO: handle the exception
e.printStackTrace();
}
// Initialize the GrowthBook SDK with the GBContext, features and saved groups
GBContext context = GBContext
.builder()
.featureSnapshot(featuresRepository.getFeatureSnapshot())
.attributesJson(userAttributesJson)
.build();
GrowthBook growthBook = new GrowthBook(context);
growthBook.isOn("featureKey");Threading note. Listeners registered directly on a repository (
featuresRepository.addFeatureRefreshListener(...)) run synchronously on the refresh thread — the polling scheduler, the SSE event thread, or the caller of a manual refresh. A slow listener delays the next refresh, and a blocking one stalls feature updates. Keep the callback short or hand the event off to your own executor.This does not apply to
GrowthBookClient.addFeatureRefreshListener(...)/subscribeFeatureRefreshListener(...): the client dispatches those off the refresh thread, on a dedicated daemon thread or on the executor you pass viaOptions.featureRefreshListenerExecutor.
GBContext needs both the features and the saved groups from the payload. Without the saved
groups, rules that reference them by ID match nobody (or, for exclusions, everybody).
| Source | Builder call |
|---|---|
GBFeaturesRepository |
.featureSnapshot(repo.getFeatureSnapshot()) |
NativeJavaGbFeatureRepository |
.featuresJson(repo.getFeaturesJson()).savedGroupsJson(repo.getSavedGroupsJson()) |
| Your own fetch | .featuresJson(...) and .savedGroupsJson(...) from the response's features and savedGroups |
GrowthBookClient handles this itself. After a refresh, build a new GBContext from the repository.
The SDK supports multiple cache modes for persisting feature payloads:
- FILE: persist to a writable directory (configurable). Defaults to a safe OS-specific cache dir (or
java.io.tmpdir). - MEMORY: in-process cache only (no filesystem writes).
- NONE: no cache I/O. Runtime still holds the latest fetched features.
- CUSTOM: supply your own
GbCacheManagerimplementation.
You can configure these through Options when using GrowthBookClient, or via the repository builder’s cacheManager directly. When cache is disabled (isCacheDisabled=true), the repository won’t attempt any persistence.
The optional growthbook-cache-caffeine module provides a Caffeine-backed GbCacheManager for applications that want a production-ready in-process cache without using filesystem persistence.
dependencies {
implementation 'com.github.growthbook:growthbook-sdk-java:<version>'
implementation 'com.github.growthbook:growthbook-cache-caffeine:<version>'
}import growthbook.sdk.java.cache.caffeine.CaffeineGbCacheManager;
import growthbook.sdk.java.multiusermode.GrowthBookClient;
import growthbook.sdk.java.multiusermode.configurations.Options;
import growthbook.sdk.java.sandbox.CacheMode;
import java.time.Duration;
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.cacheMode(CacheMode.CUSTOM)
.cacheManager(CaffeineGbCacheManager.builder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(30))
.buildManager())
.build();
GrowthBookClient gb = new GrowthBookClient(options);With the SWR strategy, the repository will:
- Perform an initial synchronous fetch during
initialize(). - Start a lightweight background poller that revalidates features on a fixed delay (by default equal to the TTL). The poller is protected against overlapping runs and logs start/end of each polling cycle.
- Keep the latest features in memory and publish
FeatureRefreshListenerevents when updated so theGlobalContextstays fresh.
For SSE connections, the repository establishes a server‑sent events stream and updates as changes arrive; the SWR poller is not used.
- The
evalFeature()method evaluates a feature based on the provided parameters. It takes three arguments: a string representing the unique identifier of the feature, a generic class valueTypeClass that specifies the type of the result value (e.g., Integer, String, Boolean), an UserContext object, which contains attributes, forceVariations and forceFeatureValues to provide a more flexible way of evaluating features. The method returns a FeatureResult object, which contains the evaluated result of the feature along with any additional metadata.
public <ValueType> FeatureResult<ValueType> evalFeature(String key, Class<ValueType> valueTypeClass, UserContext userContext);getFeatureValue()the same purpose as inevalFeature(), but have ability to provide default value
public <ValueType> ValueType getFeatureValue(String featureKey, ValueType defaultValue, Class<ValueType> gsonDeserializableClass, UserContext userContext);- The
isOn()/isOff()method takes a string argument, which is the unique identifier for the feature, and UserContext, which contains attributes, forceVariations and forceFeatureValues to provide a more flexible way of evaluating features. Functions return the feature state on/off
public Boolean isOn(String featureKey, UserContext userContext);
public Boolean isOff(String featureKey, UserContext userContext);- The
run()method takes an Experiment object and UserContext. Function returns an ExperimentResult
public <ValueType> ExperimentResult<ValueType> run(Experiment<ValueType> experiment, UserContext userContext);- If you changed, added or removed any features, trigger a feature refresh. A forced refresh always performs a
network request in the background and bypasses cache freshness checks, while current features remain available
for evaluation. For remote evaluation payloads, use
refreshForRemoteEval(...).
growthBook.refreshFeatures(RefreshMode.FORCE);
growthBook.refreshForRemoteEval(requestBodyForRemoteEval);- You can optionally configure a minimum interval between non-forced background refreshes and customize the bounded exponential retry policy. Both options are disabled/defaulted when omitted.
Options options = Options.builder()
.apiHost("https://cdn.growthbook.io")
.clientKey("sdk-abc123")
.backgroundFetchInterval(Duration.ofHours(48))
.retryPolicy(new FeatureFetchRetryPolicy(
5,
Duration.ofSeconds(1),
Duration.ofSeconds(16)
))
.build();This mode brings the security benefits of a backend SDK to the front end by evaluating feature flags exclusively on a private server. Using Remote Evaluation ensures that any sensitive information within targeting rules or unused feature variations are never seen by the client. Note that Remote Evaluation should not be used in a backend context.
You must enable Remote Evaluation in your SDK Connection settings. Cloud customers are also required to self-host a GrowthBook Proxy Server or custom remote evaluation backend.
To use Remote Evaluation, set the FeatureRefreshStrategy = REMOTE_EVAL_STRATEGY property to your Repository or Options instance. A new evaluation API call will be
made any time a user attribute or other dependency changes.
If you would like to implement Sticky Bucketing while using Remote Evaluation, you must configure your remote evaluation backend to support Sticky Bucketing. You will not need to provide a StickyBucketService instance to the client side SDK.
By default, GrowthBook does not persist assigned experiment variations for a user. We rely on deterministic hashing to ensure that the same user attributes always map to the same experiment variation. However, there are cases where this isn't good enough. For example, if you change targeting conditions in the middle of an experiment, users may stop being shown a variation even if they were previously bucketed into it. Sticky Bucketing is a solution to these issues. You can provide a Sticky Bucket Service to the GrowthBook instance to persist previously seen variations and ensure that the user experience remains consistent for your users.
Sticky bucketing ensures that users see the same experiment variant, even when user session, user login status, or
experiment parameters change. See the Sticky Bucketing docs for more
information. If your organization and experiment supports sticky bucketing, you can implement an instance of
the StickyBucketService to use Sticky Bucketing. For simple bucket persistence using the CachingLayer.
Sticky Bucket documents contain three fields:
- attributeName - The name of the attribute used to identify the user (e.g. id, cookie_id, etc.)
- attributeValue - The value of the attribute (e.g. 123)
- assignments - A dictionary of persisted experiment assignments. For example: {"exp1__0":"control"}
The attributeName/attributeValue combo is the primary key.
Here's an example implementation using a theoretical db object:
public class InMemoryStickyBucketServiceImpl implements StickyBucketService {
private final Map<String, StickyAssignmentsDocument> localStorage;
/**
* Constructs a new {@code InMemoryStickyBucketServiceImpl} with the specified local storage.
*
* @param localStorage a map to store sticky assignments documents in memory.
*/
public InMemoryStickyBucketServiceImpl(Map<String, StickyAssignmentsDocument> localStorage) {
this.localStorage = localStorage;
}
/**
* Method for getting all assignments document from cache (in memory: hashmap)
*
* @param attributeName attributeName with attributeValue together present
* a key that us for find proper StickyAssignmentsDocument
* @param attributeValue attributeName with attributeValue together present
* a key that us for find proper StickyAssignmentsDocument
* @return StickyAssignmentsDocument
*/
@Override
public StickyAssignmentsDocument getAssignments(String attributeName, String attributeValue) {
return localStorage.get(attributeName + "||" + attributeValue);
}
/**
* Method for saving assignments document to cache (in memory: hashmap)
*
* @param doc StickyAssignmentsDocument
*/
@Override
public void saveAssignments(StickyAssignmentsDocument doc) {
localStorage.put(doc.getAttributeName() + "||" + doc.getAttributeValue(), doc);
}
/**
* Method for getting sticky bucket assignments from cache (in memory: hashmap) by attributes of context
*
* @param attributes Map of String key and String value that you have in GBContext
* @return Map with key String and value StickyAssignmentsDocument
*/
@Override
public Map<String, StickyAssignmentsDocument> getAllAssignments(Map<String, String> attributes) {
Map<String, StickyAssignmentsDocument> docs = new HashMap<>();
for (Map.Entry<String, String> entry : attributes.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
StickyAssignmentsDocument doc = getAssignments(key, value);
if (doc != null) {
String docKey = doc.getAttributeName() + "||" + doc.getAttributeValue();
docs.put(docKey, doc);
}
}
return docs;
}
}We use Release-Please to automate our release process. The release process follows these steps:
-
Make changes using Conventional Commits: When making changes, format your commit messages following the Conventional Commits spec:
fix: message- for bug fixes (triggers a patch version bump)feat: message- for new features (triggers a minor version bump)feat!: messageorfix!: message- for breaking changes (triggers a major version bump)docs: message- for documentation changes (no version bump)chore: message- for maintenance changes (no version bump)
-
Automated Release PR: When commits are pushed to the
mainbranch, Release-Please will automatically create or update a release PR that:- Updates the version in
gradle.propertiesandVersion.java - Updates the
CHANGELOG.mdwith all the changes since the last release - Groups changes by type (features, fixes, etc.)
- Updates the version in
-
Review and Merge: Review the Release PR and merge it when ready to trigger a release.
-
Automated Release: Upon merging the Release PR, the action will:
- Create a Git tag for the new version
- Create a GitHub Release with release notes
- Upload build artifacts to the GitHub Release
-
Jitpack Integration: Jitpack will automatically detect the new release tag and make it available for download.
No manual version updates are required!
