> ## Documentation Index
> Fetch the complete documentation index at: https://docs.growthbook.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Java SDK

> Install the GrowthBook Java SDK to evaluate feature flags and run A/B tests in JVM applications, including targeting attributes and tracking callbacks.

This supports Java applications using Java version 1.8 and higher.

java

Java SDK Resources

Get help on Slack

> [!WARNING]
>
> **Upgrading from 0.10.x**
>
> 0.11.0 upgrades OkHttp from 4.11.0 to 5.4.0 ([#218](https://github.com/growthbook/growthbook-sdk-java/pull/218)). The `lib` module publishes these runtime dependencies:
>
> * `com.squareup.okhttp3:okhttp:5.4.0`
> * `com.squareup.okhttp3:okhttp-sse:5.4.0`
>
> OkHttp 5 also brings in `kotlin-stdlib` 2.x and `okio` 3.x transitively. If your application or another library declares OkHttp 4.x, move it to 5.4.0 as well:
>
> * **Gradle** reads the OkHttp Gradle module metadata, so `com.squareup.okhttp3:okhttp` keeps resolving to a single version. A forced or BOM-managed 4.x version wins over the 5.4.0 this SDK requests.
> * **Maven** does not read that metadata. At 5.x, the `com.squareup.okhttp3:okhttp` jar contains no classes. The JVM classes are in `com.squareup.okhttp3:okhttp-jvm`, which the SDK gets through `okhttp-sse`. Do not keep a direct `okhttp:4.x` dependency next to the SDK, because both versions end up on the classpath. If you depend on OkHttp directly, use `com.squareup.okhttp3:okhttp-jvm:5.4.0`.
> * **Spring Boot 3.3 and earlier** (including 2.7) manage `okhttp.version` as 4.x in their dependency BOM, which downgrades the SDK's OkHttp. Override it, for example with `ext['okhttp.version'] = '5.4.0'` in Gradle or `<okhttp.version>5.4.0</okhttp.version>` in Maven. Spring Boot 3.4 and later no longer manage OkHttp.
>
> The published coordinates changed in the same release. 0.10.x installed `com.github.growthbook:growthbook-sdk-java`. At 0.11.0 that coordinate is a JitPack aggregator POM: it depends on `com.github.growthbook.growthbook-sdk-java:lib`, plus the Caffeine and JCache adapter modules, which also pull in `com.github.ben-manes.caffeine:caffeine:2.9.3` and `javax.cache:cache-api:1.1.1`. Depend on `lib` for the SDK itself, and add an adapter module only when you want that cache.
>
> Other changes to check when upgrading:
>
> * `GrowthBookClient.initialize()` validates `Options` and returns `false` for settings 0.10.x accepted. See [Configuration validation](#configuration-validation).
> * Feature fetches retry by default. A failing synchronous `initialize()` or `refreshFeatures()` can block for about 15 seconds before it fails. See [Customizing refresh timing and retries](#customizing-refresh-timing-and-retries).
> * Server-Sent Events reconnects stop after `retryPolicy` max attempts consecutive failures (default: 5). 0.10.x reconnected indefinitely.
> * Each `GrowthBookClient` has its own features repository. In 0.10.x, all clients in a JVM shared the repository of the first client that was initialized.
> * `TrackData.getResult()` returns `ExperimentResult<T>` instead of `FeatureResult<T>`. Custom implementations of `IGrowthBook` must implement the new `FeatureKey` methods.

## Installation

[![JitPack version badge for the GrowthBook Java SDK](https://jitpack.io/v/growthbook/growthbook-sdk-java.svg)](https://jitpack.io/#growthbook/growthbook-sdk-java)

### 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 title="Groovy build.gradle" theme={null}
allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    implementation 'com.github.growthbook.growthbook-sdk-java:lib:0.11.0'
}
```

```kotlin title="Kotlin build.gradle.kts" theme={null}
repositories {
    maven {
        setUrl("https://jitpack.io")
    }
}

dependencies {
    implementation("com.github.growthbook.growthbook-sdk-java:lib:0.11.0")
}
```

### Maven

To install in a Maven project, add Jitpack to your repositories:

```xml theme={null}
<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:

```xml theme={null}
<dependency>
    <groupId>com.github.growthbook.growthbook-sdk-java</groupId>
    <artifactId>lib</artifactId>
    <version>0.11.0</version>
</dependency>
```

## Usage

There are two main approaches to using the GrowthBook Java SDK:

1. **Enhanced Client (Recommended)** - For better performance with multi-context support
2. **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

```java theme={null}
// 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()
);

// When the client is no longer needed, release networking and background refresh.
gb.shutdown();
```

#### Configuring the client with Options

The `Options` builder configures the `GrowthBookClient`. `apiHost` and `clientKey` are required. `apiHost` must use `http` or `https`; a host without a scheme is treated as `https`.

| Field name | Type | Description |
| - | - | - |
| `apiHost` | `String` | GrowthBook API host. Required by `GrowthBookClient`. Use `https://cdn.growthbook.io` for GrowthBook Cloud. |
| `clientKey` | `String` | Your SDK Connection client key, for example `sdk-abc123`. |
| `decryptionKey` | `String` | Decryption key for encrypted SDK payloads. See [Working with Encrypted features](#working-with-encrypted-features). |
| `enabled` | `Boolean` | Whether the SDK is enabled (default: `true`). |
| `isQaMode` | `Boolean` | QA mode. When `true`, random assignment is disabled and only forced variations are used (default: `false`). |
| `allowUrlOverrides` | `Boolean` | Allow forcing feature and experiment values via the URL (default: `false`). See [Overriding Feature Values](#overriding-feature-values). |
| `url` | `String` | URL used for URL overrides and sent as `url` in remote evaluation requests. |
| `refreshStrategy` | `FeatureRefreshStrategy` | `STALE_WHILE_REVALIDATE` (default), `SERVER_SENT_EVENTS`, or `REMOTE_EVAL_STRATEGY`. See [Caching and refreshing behavior](#caching-and-refreshing-behavior). |
| `swrTtlSeconds` | `Integer` | How often features are considered stale when using `STALE_WHILE_REVALIDATE` (default: `60`). |
| `backgroundFetchInterval` | `Duration` | Optional minimum interval between non-forced background refreshes. |
| `retryPolicy` | `FeatureFetchRetryPolicy` | Optional bounded exponential-backoff policy for feature fetches. |
| `cacheMode` | `CacheMode` | `AUTO` (default), `NONE`, `MEMORY`, `FILE`, or `CUSTOM`. See [Caching feature payloads](#caching-feature-payloads). |
| `cacheDirectory` | `String` | Directory used when `cacheMode` is `FILE`. |
| `cacheManager` | `GbCacheManager` | A custom cache implementation, used when `cacheMode` is `CUSTOM`. |
| `isCacheDisabled` | `Boolean` | Disables cache persistence entirely (default: `false`). |
| `stickyBucketService` | `StickyBucketService` | Enables [Sticky Bucketing](#sticky-bucketing). |
| `stickyBucketIdentifierAttributes` | `List<String>` | Accepted, but not used for sticky bucket lookups in 0.11.0. `GrowthBookClient` looks up assignments for every primitive user attribute. |
| `trackingCallBackWithUser` | `TrackingCallbackWithUser` | Invoked when a user is included in an experiment. The builder method name is `trackingCallBackWithUser`. See [Tracking Callback](#tracking-callback). |
| `featureUsageCallbackWithUser` | `FeatureUsageCallbackWithUser` | Invoked every time a feature is evaluated. See [Feature Usage Callback](#feature-usage-callback). |
| `featureRefreshCallback` | `FeatureRefreshCallback` | Invoked when features are refreshed in the background. |
| `remoteEval` | `Boolean` | Enables [Remote Evaluation](#remote-evaluation) (default: `false`). |
| `globalAttributes` | `JsonObject` | Default attributes applied to every evaluation, merged with the per-request `UserContext`. |
| `globalForcedFeatureValues` | `Map<String, Object>` | Globally force feature values (used for QA). |
| `globalForcedVariationsMap` | `Map<String, Integer>` | Globally force experiment variations (used for QA). |
| `cacheKeyAttributes` | `List<String>` | Attribute names included in the remote evaluation cache key. |
| `remoteEvalCacheSize` | `Integer` | Maximum number of remote evaluation responses kept in memory. |
| `remoteEvalCacheTtlSeconds` | `Integer` | Hard expiry in seconds for cached remote evaluation responses. `null` disables time-based expiry. |
| `plugins` | `List<GrowthBookPlugin>` | Plugins registered with this client. |

#### Configuration validation

`GrowthBookClient.initialize()` validates `Options` before it fetches features. `OptionsValidator.validate(options)` throws `InvalidOptionsException` (an `IllegalArgumentException`) and lists every problem on `getViolations()`. `initialize()` catches that exception, logs it, and returns `false`.

Checks include:

* `apiHost` is present and uses `http` or `https` (a host without a scheme is treated as `https`)
* `clientKey` is present
* `swrTtlSeconds` is greater than `0` when set
* `backgroundFetchInterval` is not negative when set
* `remoteEvalCacheTtlSeconds` is greater than `0` when set
* Cache settings do not conflict (`CacheMode.CUSTOM` without a `cacheManager`, or a `cacheManager` while caching is disabled)
* Remote evaluation constraints, listed under [Remote Evaluation](#remote-evaluation)

```java theme={null}
import growthbook.sdk.java.exception.InvalidOptionsException;
import growthbook.sdk.java.multiusermode.configurations.OptionsValidator;

Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        .build();

try {
    OptionsValidator.validate(options);
} catch (InvalidOptionsException e) {
    for (String problem : e.getViolations()) {
        logger.error(problem);
    }
}

GrowthBookClient gb = new GrowthBookClient(options);
if (!gb.initialize()) {
    // Invalid options, or the initial feature fetch failed.
}
```

#### Using the Enhanced Client with Different User Contexts

```java theme={null}
// 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", String.class, 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", String.class, 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](#enhanced-client-recommended) instead for better performance.**

There are 2 steps to initializing the traditional GrowthBook SDK:

1. Create a GrowthBook context `GBContext` with the features JSON and the user attributes
2. 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](#attributes). |
| `featuresJson` | `String` | The features JSON served by the GrowthBook API (or equivalent). See [Features](#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](#tracking-callback). To subscribe to all evaluated events regardless of whether the user is in the experiment, see [Subscribing to experiment runs](#experiment-run-callback). |
| `featureUsageCallback` | `FeatureUsageCallback` | A callback that will be invoked every time a feature is viewed. See [FeatureUsageCallback](#feature-usage-callback) |
| `encryptionKey` | `String` | Decryption key for encrypted SDK payloads. See [Working with Encrypted features](#working-with-encrypted-features). |
| `allowUrlOverrides` | `Boolean` | Allow forcing feature and experiment values via the URL (default: `false`). See [Overriding Feature Values](#overriding-feature-values). |
| `stickyBucketService` | `StickyBucketService` | Enables [Sticky Bucketing](#sticky-bucketing). |
| `stickyBucketIdentifierAttributes` | `List<String>` | Accepted, but not used for sticky bucket lookups in 0.11.0. `GrowthBook` derives lookup attributes from each experiment's `hashAttribute` and `fallbackAttribute`. |
| `savedGroups` | `JsonObject` | Saved Groups data, when not embedded in the features payload. |
| `apiHost` | `String` | API host used for [Remote Evaluation](#remote-evaluation). |
| `clientKey` | `String` | SDK client key used for remote evaluation. |
| `remoteEval` | `Boolean` | Enables remote evaluation (default: `false`). |
| `cacheKeyAttributes` | `List<String>` | Attribute names included in the remote evaluation cache key. |
| `remoteEvalCacheSize` | `Integer` | Maximum number of remote evaluation responses kept in memory. |
| `remoteEvalCacheTtlSeconds` | `Integer` | Hard expiry in seconds for cached remote evaluation responses. `null` disables time-based expiry. |
| `plugins` | `List<GrowthBookPlugin>` | Plugins registered with this context. |

#### 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 Java theme={null}
// 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);
```

```kotlin Kotlin theme={null}
// 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)
```

> [!NOTE]
>
> **HttpClient recommendation**
>
> 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 Java theme={null}
// 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, // attributesJson
    null,               // attributes
    featuresJson,       // featuresJson
    null,               // features
    null,               // encryptionKey
    isEnabled,          // enabled
    isQaMode,           // isQaMode
    url,                // url
    false,              // allowUrlOverrides
    forcedVariations,   // forcedVariationsMap
    trackingCallback,   // trackingCallback
    null,               // featureUsageCallback
    null,               // stickyBucketService
    null,               // stickyBucketAssignmentDocs
    null,               // stickyBucketIdentifierAttributes
    null                // savedGroups
);

GrowthBook growthBook = new GrowthBook(context);
```

```kotlin Kotlin theme={null}
// 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,   // attributesJson
    null,             // attributes
    featuresJson,     // featuresJson
    null,             // features
    null,             // encryptionKey
    isEnabled,        // enabled
    isQaMode,         // isQaMode
    url,              // url
    false,            // allowUrlOverrides
    forcedVariations, // forcedVariationsMap
    trackingCallback, // trackingCallback
    null,             // featureUsageCallback
    null,             // stickyBucketService
    null,             // stickyBucketAssignmentDocs
    null,             // stickyBucketIdentifierAttributes
    null              // savedGroups
)

val growthBook = GrowthBook(context)
```

For complete examples, see the [Examples](#code-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](https://docs.growthbook.io/app/features)
* You can see an [example features JSON here](https://docs.growthbook.io/app/api.md#sdk-connection-endpoints)

#### Attributes

Attributes are a JSON string. You can specify attributes about the current user and request. Here's an example:

```java Java theme={null}
String userAttributes = "{\"country\": \"canada\", \"id\": \"user-abc123\"}";
```

```kotlin Kotlin theme={null}
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.

```java Enhanced Client theme={null}
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", String.class, userContext);
```

```java Traditional Client theme={null}
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, T defaultValue)`** | generic `T` (nullable) | Returns the value cast to the generic type. Type is inferred based on the `defaultValue` argument provided. |
| **`evalFeature(String, Class<T>)`** | `FeatureResult<T>` | Returns a feature result with more info about why the value was assigned. |

`GrowthBookClient` methods take a `UserContext`. On that client, `getFeatureValue` and `evalFeature` also take a `Class<T>`, such as `String.class`. `GrowthBook.evalFeature` takes a `Class<T>` as well. The primitive `GrowthBook.getFeatureValue` overloads still infer the type from `defaultValue`.

```java Enhanced Client theme={null}
// 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, Float.class, userContext);
String theme = gb.getFeatureValue("theme", "light", String.class, userContext);
Integer maxItems = gb.getFeatureValue("max_items", 10, Integer.class, userContext);

// Get detailed feature result
FeatureResult<Float> priceResult = gb.evalFeature("donut_price", Float.class, userContext);
Object price = priceResult.getValue();
FeatureResultSource source = priceResult.getSource();
```

```java Traditional Client theme={null}
// 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.evalFeature("donut_price", Float.class);
Object price = priceResult.getValue();
FeatureResultSource source = priceResult.getSource();
```

#### 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](https://growthbook.github.io/growthbook-sdk-java/growthbook/sdk/java/GrowthBook.html) for more information.

[See the unit tests ](https://github.com/growthbook/growthbook-sdk-java/blob/main/lib/src/test/java/growthbook/sdk/java/GrowthBookTest.java#L220) 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>` has the following getters. `getValue()` returns the raw evaluated value as `Object`, so cast it yourself, or use `getFeatureValue` when you only need the typed value.

| Method | Return type | Description |
| - | - | - |
| **`getValue()`** | `Object` (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.

#### Typed feature access

**Available starting in version 0.11.0**

`FeatureKey` carries both the feature identifier and its value type. Declare keys once with `TypedKey`, then pass them to `GrowthBookClient` or `GrowthBook` instead of raw strings.

```java theme={null}
import growthbook.sdk.java.model.FeatureKey;
import growthbook.sdk.java.model.TypedKey;

public final class Features {
    public static final FeatureKey<Boolean> NEW_HOME = TypedKey.ofBoolean("new-home");
    public static final FeatureKey<String> THEME = TypedKey.ofString("theme");
    public static final FeatureKey<Integer> MAX_ITEMS = TypedKey.ofInteger("max-items");
    public static final FeatureKey<MyConfig> CONFIG = TypedKey.of("my-config", MyConfig.class);

    private Features() {}
}
```

```java Enhanced Client theme={null}
boolean newHome = gb.getBooleanFeature(Features.NEW_HOME, userContext);
String theme = gb.getStringFeature(Features.THEME, "light", userContext);
Integer maxItems = gb.getIntegerFeature(Features.MAX_ITEMS, 10, userContext);
FeatureResult<Boolean> result = gb.getFeature(Features.NEW_HOME, userContext);
```

```java Traditional Client theme={null}
boolean newHome = growthBook.getBooleanFeature(Features.NEW_HOME);
String theme = growthBook.getStringFeature(Features.THEME, "light");
Integer maxItems = growthBook.getIntegerFeature(Features.MAX_ITEMS, 10);
FeatureResult<Boolean> result = growthBook.getFeature(Features.NEW_HOME);
```

`getFeature` returns the raw evaluated value. It does not deserialize that value into the key's type. Use `getFeatureValue(FeatureKey, defaultValue, UserContext)` on `GrowthBookClient`, or `getFeatureValue(FeatureKey, defaultValue)` on `GrowthBook`, when you need a deserialized object. `TypedKey` also provides `ofDouble` and `ofFloat`. The matching client methods are `getDoubleFeature` and `getFloatFeature`.

### 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.

The `Experiment` builder takes `ArrayList` values for `variations` and `weights`, and a Gson `JsonObject` for `conditionJson`.

```java Enhanced Client theme={null}
// 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(new ArrayList<>(Arrays.asList(2.50f, 3.00f, 3.50f)))
    .weights(new ArrayList<>(Arrays.asList(0.33f, 0.33f, 0.34f)))
    .conditionJson(JsonParser.parseString("{\"employee\": true}").getAsJsonObject())
    .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(new ArrayList<>(Arrays.asList("blue", "red", "green")))
    .build();

ExperimentResult<String> colorResult = gb.run(buttonColorExperiment, userContext);
String buttonColor = colorResult.getValue();
```

```java Traditional Client theme={null}
// 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(new ArrayList<>(Arrays.asList(2.50f, 3.00f, 3.50f)))
    .weights(new ArrayList<>(Arrays.asList(0.33f, 0.33f, 0.34f)))
    .conditionJson(JsonParser.parseString("{\"employee\": true}").getAsJsonObject())
    .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(new ArrayList<>(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.

### GrowthBook Event Tracking

Sends experiment view and feature usage events to GrowthBook's [Managed Warehouse](https://docs.growthbook.io/app/managed-warehouse.md) or [Event Forwarder](https://docs.growthbook.io/app/event-forwarder.md).

Requires the GrowthBook Java SDK v0.11.1 or later.

```java theme={null}
import growthbook.sdk.java.multiusermode.GrowthBookClient;
import growthbook.sdk.java.multiusermode.configurations.Options;
import growthbook.sdk.java.multiusermode.configurations.UserContext;
import growthbook.sdk.java.plugin.tracking.GrowthBookTrackingPlugin;
import growthbook.sdk.java.plugin.tracking.TrackingPluginConfig;
import java.util.Collections;

String clientKey = "YOUR_CLIENT_KEY";

GrowthBookTrackingPlugin trackingPlugin = GrowthBookTrackingPlugin.of(
    TrackingPluginConfig.builder()
        .clientKey(clientKey)
        .ingestorHost("https://us-east-1.gb-ingest.com")
        .build()
);

GrowthBookClient client = new GrowthBookClient(
    Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey(clientKey)
        .plugins(Collections.singletonList(trackingPlugin))
        .build()
);

if (!client.initialize()) {
    client.shutdown();
    throw new IllegalStateException("GrowthBook initialization failed");
}

UserContext user = UserContext.builder()
    .attributesJson("{\"id\":\"user-123\"}")
    .build();

boolean enabled = client.isOn("my-feature", user);
```

The plugin automatically sends feature usage and experiment view events, with the user's attributes as context. The plugin doesn't inherit the client's SDK connection key, so pass it to both `TrackingPluginConfig` and `Options`.

Keep one client for your application and create a separate tracking plugin for each client. At application shutdown, call `client.shutdown()` to flush pending events and release resources:

```java theme={null}
client.shutdown();
```

Custom events aren't supported yet. Send them with the [Ingestion API](https://docs.growthbook.io/app/managed-warehouse.md#ingestion-api).

| Option | Description |
| - | - |
| `ingestorHost` | Ingestor for your Data Region. |
| `batchSize` | Events to queue before sending. Default `100`. |
| `batchTimeout` | How long to wait before sending a partial batch. Default `Duration.ofSeconds(10)`. |
| `closeTimeout` | Time budget for flushing events during shutdown. Default `Duration.ofSeconds(5)`. |

### 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:

1. **Tracking Callback** - Called when a user is included in an experiment
2. **Feature Usage Callback** - Called every time a feature is evaluated
3. **Experiment Run Callback** - Called when an inline experiment is run with `run()` (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.

```java Enhanced Client theme={null}
// Create tracking callback. Options uses TrackingCallbackWithUser,
// including the UserContext for this evaluation.
TrackingCallbackWithUser trackingCallback = new TrackingCallbackWithUser() {
    @Override
    public <ValueType> void onTrack(
        Experiment<ValueType> experiment,
        ExperimentResult<ValueType> experimentResult,
        UserContext userContext
    ) {
        // Send to your analytics system
        Map<String, Object> properties = new HashMap<>();
        properties.put("experiment_id", experiment.getKey());
        properties.put("variation_id", experimentResult.getVariationId());
        properties.put("hash_attribute", experimentResult.getHashAttribute()); // e.g. "id"
        properties.put("hash_value", experimentResult.getHashValue()); // e.g. the user's id
        analytics.track("experiment_viewed", properties);
    }
};

// Create enhanced client with tracking.
// The builder method is trackingCallBackWithUser.
Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        .trackingCallBackWithUser(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);
```

```java Traditional Client theme={null}
// 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.

```java Enhanced Client theme={null}
// Create feature usage callback. Options uses FeatureUsageCallbackWithUser.
FeatureUsageCallbackWithUser featureUsageCallback = new FeatureUsageCallbackWithUser() {
    @Override
    public <ValueType> void onFeatureUsage(
        String featureKey,
        FeatureResult<ValueType> featureResult,
        UserContext userContext
    ) {
        // Log feature usage
        logger.info("Feature '{}' evaluated with value: {}",
                   featureKey, featureResult.getValue());

        // Send to analytics
        Map<String, Object> properties = new HashMap<>();
        properties.put("feature_key", featureKey);
        properties.put("feature_value", featureResult.getValue());
        properties.put("source", String.valueOf(featureResult.getSource()));
        analytics.track("feature_used", properties);
    }
};

// Create enhanced client with feature usage tracking
Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        .featureUsageCallbackWithUser(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", String.class, userContext);
boolean darkMode = gb.isOn("dark_mode", userContext);
```

```java Traditional Client theme={null}
// 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 inline experiment runs using the `ExperimentRunCallback`. **This callback is called regardless of whether the user is included in the experiment**.

Subscriptions fire from `run()` only, not from feature evaluation such as `isOn()` or `evalFeature()`. They fire the first time an experiment key is run, and again only when `inExperiment` or the variation changes for that key on the same instance. On a shared `GrowthBookClient`, that state is kept per experiment key across all users, not per user.

```java Enhanced Client theme={null}
// Create experiment run callback
ExperimentRunCallback experimentRunCallback = new ExperimentRunCallback() {
    @Override
    public <ValueType> void onRun(
        Experiment<ValueType> experiment,
        ExperimentResult<ValueType> experimentResult
    ) {
        // Track inline experiment runs (included and excluded users)
        Map<String, Object> properties = new HashMap<>();
        properties.put("experiment_id", experiment.getKey());
        properties.put("user_in_experiment", experimentResult.getInExperiment());
        properties.put("variation_id", experimentResult.getVariationId());
        properties.put("hash_used", experimentResult.getHashUsed());
        analytics.track("experiment_evaluated", properties);
    }
};

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(new ArrayList<>(Arrays.asList("blue", "red", "green")))
    .build();

ExperimentResult<String> result = gb.run(buttonColorExperiment, userContext);
```

```java Traditional Client theme={null}
// Create experiment run callback
ExperimentRunCallback experimentRunCallback = new ExperimentRunCallback() {
    @Override
    public <ValueType> void onRun(
        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(new ArrayList<>(Arrays.asList("blue", "red", "green")))
    .build();

ExperimentResult<String> result = growthBook.run(buttonColorExperiment);
```

#### Multiple Callback Subscriptions

You can subscribe to multiple experiment run callbacks. `onRun` is a generic method, so implement the callback with a class or an anonymous class. A Java lambda cannot implement it.

```java Enhanced Client theme={null}
// Multiple callbacks for different purposes
ExperimentRunCallback analyticsCallback = new ExperimentRunCallback() {
    @Override
    public <ValueType> void onRun(Experiment<ValueType> experiment, ExperimentResult<ValueType> result) {
        // Send to analytics
        Map<String, Object> properties = new HashMap<>();
        properties.put("experiment", experiment.getKey());
        properties.put("included", result.getInExperiment());
        analytics.track("experiment_run", properties);
    }
};

ExperimentRunCallback loggingCallback = new ExperimentRunCallback() {
    @Override
    public <ValueType> void onRun(Experiment<ValueType> experiment, ExperimentResult<ValueType> result) {
        // Log for debugging
        logger.debug("Experiment {} evaluated for {} {}",
                experiment.getKey(), result.getHashAttribute(), result.getHashValue());
    }
};

// Subscribe multiple callbacks
gb.subscribe(analyticsCallback);
gb.subscribe(loggingCallback);
```

```java Traditional Client theme={null}
// Multiple callbacks for different purposes
ExperimentRunCallback analyticsCallback = new ExperimentRunCallback() {
    @Override
    public <ValueType> void onRun(Experiment<ValueType> experiment, ExperimentResult<ValueType> result) {
        // Send to analytics
        System.out.println("Analytics: " + experiment.getKey());
    }
};

ExperimentRunCallback loggingCallback = new ExperimentRunCallback() {
    @Override
    public <ValueType> void onRun(Experiment<ValueType> experiment, ExperimentResult<ValueType> 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](https://docs.growthbook.io/app/api.md#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 Java theme={null}
// 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);

```

```kotlin Kotlin theme={null}
// 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, Caching, 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 a decryption key, e.g. `.builder().decryptionKey(clientKey)` (`encryptionKey` on this builder is deprecated)
* Caching features (in-memory)
* Refreshing features

If you wish to manage fetching, refreshing, and caching features on your own, you can choose to not implement this class.

> [!NOTE]
>
> **Recommendation**
>
> 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 Java theme={null}
GBFeaturesRepository featuresRepository = GBFeaturesRepository
    .builder()
    .apiHost("https://cdn.growthbook.io")
    .clientKey("<environment_key>") // replace with your client key
    .decryptionKey("<client-key-for-decrypting>") // optional, nullable
    .refreshStrategy(FeatureRefreshStrategy.SERVER_SENT_EVENTS) // optional; options: STALE_WHILE_REVALIDATE, SERVER_SENT_EVENTS, REMOTE_EVAL_STRATEGY (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);
    }

    @Override
    public void onError(Throwable throwable) {
        System.out.println("Features refresh failed: " + throwable.getMessage());
    }
});

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);
```

```kotlin Kotlin theme={null}
val featuresRepository = GBFeaturesRepository
    .builder()
    .apiHost("https://cdn.growthbook.io")
    .clientKey("<environment_key>") // replace with your client key
    .decryptionKey("<client-key-for-decrypting>") // optional, nullable
    .swrTtlSeconds(30)
    .build()
    .apply {
        // Optional callback for getting updates when features are refreshed
        onFeaturesRefresh(object : FeatureRefreshCallback {
            override fun onRefresh(featuresJson: String) {
                println("Features have been refreshed \n $featuresJson")
            }

            override fun onError(throwable: Throwable) {
                println("Features refresh failed: ${throwable.message}")
            }
        })
    }

// 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](#code-examples) below.

#### Caching and refreshing behavior

There are 3 refresh strategies available, selected with `FeatureRefreshStrategy` on the `Options` or `GBFeaturesRepository` builder.

##### 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 `swrTtlSeconds` option, which also sets the interval of the background poll that drives the refresh.

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.

The repository sends `If-None-Match` and keeps the current payload when the server responds `304 Not Modified`.

```java Enhanced Client theme={null}
Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        // Consider features stale after 10 minutes
        .swrTtlSeconds(600)
        .build();

GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
```

```java Traditional theme={null}
GBFeaturesRepository featuresRepository = GBFeaturesRepository
    .builder()
    .apiHost("https://cdn.growthbook.io")
    .clientKey("sdk-abc123")
    // Consider features stale after 10 minutes
    .swrTtlSeconds(600)
    .build();
```

> [!WARNING]
>
> **Re-read `getFeaturesJson()` per request**
>
> `GBContext` holds the features JSON you pass it as a fixed snapshot. If you build a single `GBContext` at startup, refreshed features will never reach it no matter what `swrTtlSeconds` is set to. Either call `getFeaturesJson()` again when building each request's context, or use the [Enhanced Client](#enhanced-client-recommended), which handles this for you.

`swrTtlSeconds` has no effect under the `SERVER_SENT_EVENTS` refresh strategy, where updates are pushed as they happen.

##### 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 ](https://app.growthbook.io), this is ready for you to use. If you are self-hosting, you will need to set up the [GrowthBook Proxy](https://docs.growthbook.io/self-host/proxy.md) to enable it.

##### Remote Eval strategy

`FeatureRefreshStrategy.REMOTE_EVAL_STRATEGY` is a repository refresh mode. On `initialize()`, `GBFeaturesRepository` calls `fetchForRemoteEval` with the configured `RequestBodyForRemoteEval`.

That strategy is separate from `Options.remoteEval(true)`, which is the client remote evaluation path described in [Remote Evaluation](#remote-evaluation). Setting only `REMOTE_EVAL_STRATEGY` does not set `remoteEval`. When `remoteEval(true)` is set, `GrowthBookClient.initialize()` does not create a features repository for evaluation, so this strategy does not apply.

#### Caching feature payloads

The SDK can persist the fetched feature payload so a restart does not require a fresh network call before features are available. Configure this with `cacheMode` (`CacheMode`):

| Mode | Behavior |
| - | - |
| `AUTO` | Default. Uses `FILE` when a writable directory is available, otherwise `MEMORY`. |
| `FILE` | Persists the payload to a directory on disk. Falls back to `MEMORY` if it can't. |
| `MEMORY` | In-process memory cache only. No filesystem writes. |
| `NONE` | No cache persistence. The runtime still keeps the latest fetched features in memory. |
| `CUSTOM` | Use a `GbCacheManager` supplied with `cacheManager`. |

```java theme={null}
Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        .cacheMode(CacheMode.FILE)
        .cacheDirectory("/var/cache/growthbook")
        .build();
```

`AUTO` and `FILE` use `cacheDirectory` when it is set. Otherwise `AUTO` tries the `growthbook.cache.dir` system property, then the platform cache directory, then `java.io.tmpdir`, so it usually writes to disk.

`isCacheDisabled(true)` turns persistence off. `CacheMode.CUSTOM` requires a `cacheManager`. Supplying a `cacheManager` while caching is disabled fails [configuration validation](#configuration-validation).

##### Caffeine cache adapter

The optional `growthbook-cache-caffeine` module is a Caffeine-backed `GbCacheManager`. It depends on `com.github.ben-manes.caffeine:caffeine:2.9.3`. Published on JitPack as `com.github.growthbook.growthbook-sdk-java:growthbook-cache-caffeine:0.11.0`. That module already depends on `lib`.

```groovy theme={null}
dependencies {
    implementation 'com.github.growthbook.growthbook-sdk-java:growthbook-cache-caffeine:0.11.0'
}
```

```java theme={null}
import growthbook.sdk.java.cache.caffeine.CaffeineGbCacheManager;

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();
```

`CaffeineGbCacheManager.builder()` returns `CaffeineCacheOptions.Builder`. `buildManager()` returns the cache manager. `maximumSize` defaults to `1000`. `maximumWeight(long)` bounds the cache by UTF-8 payload bytes and takes precedence over `maximumSize`. `recordStats(true)` turns on the counters reported by `stats()`; without it they stay at `0`.

##### JCache adapter

The optional `growthbook-cache-jcache` module wraps a JSR-107 cache. Published on JitPack as `com.github.growthbook.growthbook-sdk-java:growthbook-cache-jcache:0.11.0`. It depends on `javax.cache:cache-api:1.1.1` and on `lib`. It does not include a cache provider. Put a JSR-107 provider such as Ehcache or Hazelcast on the runtime classpath.

```groovy theme={null}
dependencies {
    implementation 'com.github.growthbook.growthbook-sdk-java:growthbook-cache-jcache:0.11.0'
}
```

```java theme={null}
import growthbook.sdk.java.cache.jcache.JCacheGbCacheManager;
import javax.cache.Caching;
import javax.cache.CacheManager;

CacheManager cacheManager = Caching.getCachingProvider().getCacheManager();

Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        .cacheMode(CacheMode.CUSTOM)
        .cacheManager(JCacheGbCacheManager.builder()
                .cacheManager(cacheManager)
                .buildManager())
        .build();
```

`JCacheGbCacheManager.builder()` returns `JCacheCacheOptions.Builder`. `cacheManager` is required. `cacheName` defaults to `growthbook-features`. Eviction and expiry are configured on the provider, not on this adapter.

#### Customizing refresh timing and retries

You can set a minimum interval between non-forced background refreshes and a bounded exponential retry policy for feature fetches. When `retryPolicy` is omitted, the repository uses `FeatureFetchRetryPolicy`'s default: 5 total attempts, with retry waits of 1s, 2s, 4s, and 8s. The default maximum delay is 16 seconds. Network errors, `408`, `429`, and `5xx` responses are retried. Retries run on the calling thread, so a failing `initialize()` can block for about 15 seconds with the default policy. The same `maxAttempts` limits consecutive Server-Sent Events reconnect attempts.

```java theme={null}
Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        .backgroundFetchInterval(Duration.ofHours(48))
        .retryPolicy(new FeatureFetchRetryPolicy(
            5,                       // max attempts, including the first
            Duration.ofSeconds(1),   // delay before the first retry
            Duration.ofSeconds(16)   // upper bound for any retry delay
        ))
        .build();
```

#### Forcing a refresh

`GrowthBookClient.refreshFeatures()` honors the configured background fetch interval. `refreshFeatures(RefreshMode.FORCE)` always performs a network request and bypasses cache freshness checks. Current features stay available for evaluation while the refresh runs.

```java theme={null}
gb.refreshFeatures();
gb.refreshFeatures(RefreshMode.FORCE);
```

`refreshFeature()` is deprecated and calls `refreshFeatures()`, except when remote evaluation is enabled. In that case it clears the remote evaluation cache.

## Sticky Bucketing

By default, GrowthBook does not persist assigned experiment variations for a user. It relies on deterministic hashing so the same user attributes always map to the same experiment variation. That is not always enough. 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 keeps users on the same experiment variant when user attributes, login status, or experiment parameters change. See the [Sticky Bucketing docs](https://docs.growthbook.io/app/sticky-bucketing.md).

Provide a `StickyBucketService`. The SDK includes `InMemoryStickyBucketServiceImpl`. Implement the interface yourself for a database or other store.

The SDK chooses which attributes to look up. `GrowthBookClient` requests assignments for every primitive user attribute. `GrowthBook` uses each experiment's `hashAttribute` and `fallbackAttribute`. The `stickyBucketIdentifierAttributes` option is accepted but not used for this in 0.11.0.

A `StickyAssignmentsDocument` has three fields:

* `attributeName`: the attribute used to identify the user, such as `id` or `cookie_id`
* `attributeValue`: the value of that attribute, such as `123`
* `assignments`: persisted experiment assignments, for example `{"exp1__0":"control"}`

`attributeName` and `attributeValue` together are the primary key.

```java Enhanced Client theme={null}
StickyBucketService stickyBucketService =
    new InMemoryStickyBucketServiceImpl(new HashMap<>());

Options options = Options.builder()
        .apiHost("https://cdn.growthbook.io")
        .clientKey("sdk-abc123")
        .stickyBucketService(stickyBucketService)
        .build();

GrowthBookClient gb = new GrowthBookClient(options);
gb.initialize();
```

```java Traditional Client theme={null}
StickyBucketService stickyBucketService =
    new InMemoryStickyBucketServiceImpl(new HashMap<>());

GBContext context = GBContext
    .builder()
    .featuresJson(featuresJson)
    .attributesJson(userAttributesJson)
    .stickyBucketService(stickyBucketService)
    .build();

GrowthBook growthBook = new GrowthBook(context);
```

### Implementing a custom StickyBucketService

Implement `StickyBucketService` to persist assignments. `getAssignments`, `saveAssignments`, and `getAllAssignments` are the interface methods. The map returned by `getAllAssignments` must be keyed by `attributeName + "||" + attributeValue`, because the SDK looks documents up by that key. `InMemoryStickyBucketServiceImpl` uses the same format.

```java theme={null}
public class MapStickyBucketService implements StickyBucketService {
    private final Map<String, StickyAssignmentsDocument> store = new HashMap<>();

    @Override
    public StickyAssignmentsDocument getAssignments(String attributeName, String attributeValue) {
        return store.get(attributeName + "||" + attributeValue);
    }

    @Override
    public void saveAssignments(StickyAssignmentsDocument doc) {
        store.put(doc.getAttributeName() + "||" + doc.getAttributeValue(), doc);
    }

    @Override
    public Map<String, StickyAssignmentsDocument> getAllAssignments(Map<String, String> attributes) {
        Map<String, StickyAssignmentsDocument> docs = new HashMap<>();
        for (Map.Entry<String, String> entry : attributes.entrySet()) {
            StickyAssignmentsDocument doc = getAssignments(entry.getKey(), entry.getValue());
            if (doc != null) {
                docs.put(doc.getAttributeName() + "||" + doc.getAttributeValue(), doc);
            }
        }
        return docs;
    }
}
```

Do not set `stickyBucketService` when `remoteEval` is enabled. Remote evaluation rejects that combination. See [Remote Evaluation](#remote-evaluation).

## Remote Evaluation

**The `remoteEval` option is available starting in version 0.11.0.** `refreshForRemoteEval` and `FeatureRefreshStrategy.REMOTE_EVAL_STRATEGY` were available earlier.

Remote Evaluation evaluates feature flags on a private server. Targeting rules and unused variations stay off the client. On a trusted server it is usually unnecessary, because the SDK can evaluate the full payload locally. It is mainly useful for Android and other clients you don't control. Enable it in the SDK Connection settings. Cloud customers need a self-hosted [GrowthBook Proxy](https://docs.growthbook.io/self-host/proxy.md) or another remote evaluation backend. Read the [remote evaluation guide](https://docs.growthbook.io/self-host/remote-evaluation.md).

On `GrowthBookClient`, set `remoteEval(true)`. `initialize()` prepares the remote evaluation client and returns `false` when setup fails. Later evaluations call the remote API for the current user attributes and cache the response. If that request fails, the SDK logs a warning and evaluates against an empty feature set, so features return their fallback values. `preloadRemoteEval(UserContext)` warms that cache and returns `false` when remote evaluation is off or the request fails.

```java theme={null}
Options options = Options.builder()
        .apiHost("https://gb-proxy.example.com")
        .clientKey("sdk-abc123")
        .remoteEval(true)
        .build();

GrowthBookClient gb = new GrowthBookClient(options);
if (!gb.initialize()) {
    // Invalid options, or remote evaluation setup failed.
}

UserContext userContext = UserContext.builder()
        .attributesJson("{\"id\":\"user_123\"}")
        .build();

gb.preloadRemoteEval(userContext);
boolean enabled = gb.isOn("new-homepage", userContext);
```

`apiHost` cannot be `growthbook.io` or a subdomain of it. `OptionsValidator` also rejects remote evaluation when:

* `decryptionKey` is set
* `stickyBucketService` is set
* `refreshStrategy` is explicitly `FeatureRefreshStrategy.STALE_WHILE_REVALIDATE`

Leave `refreshStrategy` unset, as in the example above. `remoteEval(true)` does not change that field. The validator reads `getRefreshStrategy()`, which is `null` until you set it, and `null` is allowed. `getRefreshingStrategy()` still reports `STALE_WHILE_REVALIDATE` when the field is unset, but remote evaluation does not use that default. Setting the strategy explicitly to `STALE_WHILE_REVALIDATE` is what makes `initialize()` return `false`.

Sticky bucketing for this mode belongs on the remote evaluation backend. The client must not be given a `StickyBucketService`.

`refreshForRemoteEval(RequestBodyForRemoteEval)` sends an explicit payload. `RequestBodyForRemoteEval` accepts `attributes` (`JsonObject`), `forcedFeatures`, `forcedVariations`, and `url`.

```java theme={null}
RequestBodyForRemoteEval body = new RequestBodyForRemoteEval();
body.setAttributes(attributes);
gb.refreshForRemoteEval(body);
```

With `refreshStrategy(FeatureRefreshStrategy.SERVER_SENT_EVENTS)`, the client listens for feature updates and clears the remote evaluation cache when they arrive.

`GBContext.builder().remoteEval(true)` is the same flag for a traditional `GrowthBook` instance. It also requires `apiHost` and `clientKey`. With an invalid configuration, `new GrowthBook(context)` throws `InvalidOptionsException` instead of returning a failure value.

## Custom Fields

**Available starting in version 0.11.0**

Experiments can carry custom metadata configured in GrowthBook. `Experiment` exposes:

* `getCustomField(String fieldId)` returns the raw value (`Object`), or `null` if it is absent
* `getCustomField(String fieldId, Class<FieldType> fieldType)` casts or deserializes the value with Gson
* `hasCustomField(String fieldId)` returns whether the field is present

```java theme={null}
FeatureResult<Float> result = gb.evalFeature("donut_price", Float.class, userContext);
Experiment<Float> experiment = result.getExperiment();

if (experiment != null && experiment.hasCustomField("team")) {
    String owningTeam = experiment.getCustomField("team", String.class);
}
```

`FeatureRule` stores the same metadata on `getCustomFields()`, which returns `Map<String, Object>`. It does not have the `getCustomField(String)` helpers that `Experiment` does.

## Diagnostics

**Available starting in version 0.11.0**

`GrowthBookClient.getDiagnostics()` returns a `Diagnostics` snapshot. `toJson()` serializes it. `getHealth().getState()` is a `DiagnosticsHealthState`: `READY`, `NOT_READY`, `DEGRADED`, `ERROR`, or `SHUTDOWN`. `getHealth().getIssues()` lists `DiagnosticsIssue` values, each with `getCode()`, `getSeverity()`, and `getMessage()`.

The snapshot also includes SDK, config, client, feature, refresh, cache, streaming, and remote evaluation sections through `getSdk()`, `getConfig()`, `getClient()`, `getFeatures()`, `getRefresh()`, `getCache()`, `getStreaming()`, and `getRemoteEval()`.

```java theme={null}
import growthbook.sdk.java.diagnostics.model.Diagnostics;
import growthbook.sdk.java.diagnostics.model.enums.DiagnosticsHealthState;

Diagnostics diagnostics = gb.getDiagnostics();
DiagnosticsHealthState state = diagnostics.getHealth().getState();
String json = diagnostics.toJson();
```

## 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.

```java theme={null}
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

```java theme={null}
@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, Integer.class, 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", String.class, 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

* [JVM with Spring Web example ](https://github.com/growthbook/examples/tree/main/jvm-spring-web)
* [JVM example in Kotlin with Ktor ](https://github.com/growthbook/examples/tree/main/jvm-kotlin-ktor-example)
* [Android Java example ](https://github.com/growthbook/examples/tree/main/android-example)

## Further Reading

* [JavaDoc class documentation ](https://growthbook.github.io/growthbook-sdk-java/)

## Supported Features
