Java

This SDK is designed for server-side Java applications. It uses the local evaluation paradigm, which means that feature flag evaluations are performed locally using a copy of the flag configuration stored in memory. The configuration is synchronized with the Tggl API in the background.

Info

Looking for Kotlin? It is the same SDK, but we have a dedicated documentation for it!

Installation

Add the dependency to your project:

Gradle

implementation 'io.tggl:tggl-client:1.0.0'

Maven

<dependency>
  <groupId>io.tggl</groupId>
  <artifactId>tggl-client</artifactId>
  <version>1.0.0</version>
</dependency>
Info

The SDK requires Java 8 or later.

Quick start

Create a TgglClient with your server API key:

import io.tggl.TgglClient;
import io.tggl.TgglClientOptions;
 
TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .build());
 
// Wait for the initial configuration to be fetched
client.waitReady().join();
 
// Evaluate a flag
Map<String, Object> context = Map.of("userId", "abc123");
boolean value = client.get(context, "my-feature", false);
 
if (value) {
  // ...
}

The TgglClient evaluates millions of contexts per second since no network request is performed during evaluation. The configuration is fetched once and kept up-to-date via background polling.

Creating a static client per request

For HTTP servers, you can create a TgglStaticClient for each request to avoid passing the context around:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .build());
 
// In your request handler / middleware
TgglStaticClient requestClient = client.createClientForContext(
    Map.of("userId", request.getUserId())
);
 
String value = requestClient.get("my-feature", "default_value");

The TgglStaticClient pre-computes all flag values for the given context at creation time, so calling get on it is extremely fast.

Evaluating flags

The get method evaluates a feature flag. It takes the flag slug and a default value, which is returned if the flag is not found, inactive, or if an error occurs during evaluation. The context is always passed as the first argument on TgglClient.

// On TgglClient, pass the context first
boolean enabled = client.get(context, "my-feature", false);
String variant = client.get(context, "color-button", "#000000");
 
// On TgglStaticClient, no context needed
boolean enabled = staticClient.get("my-feature", false);
String variant = staticClient.get("color-button", "#000000");

Choose the default value carefully to ensure your application behaves correctly even if the Tggl service is unreachable for any reason or if someone from your team inadvertently removes the flag entirely.

Info

The SDK performs a type check on the returned value. If the evaluated flag value does not match the type of the default value you provided, the default value is returned instead. This prevents ClassCastException at call sites.

Getting all flags

You can evaluate all flags at once for a given context:

Map<String, Object> allFlags = client.getAll(context);

This returns a map containing only active flags and their values. Inactive flags are omitted.

Waiting for the client to be ready

The client needs to load the flag configuration before it can correctly evaluate flags. If you call get before the client is ready, it will return the default value for all flags.

There are multiple ways to wait for the client to be ready:

// 1. Block the current thread (using CompletableFuture)
client.waitReady().join();
 
// 2. Async callback
client.onReady(() -> {
    // Client is ready
});
 
// 3. Check synchronously
if (client.isReady()) {
    // ...
}

The client is ready as soon as the first configuration has been loaded, either from the API or from a storage.

Polling for live updates

You can update your flags at any time from the Tggl dashboard. To get these updates in your application, the client polls the Tggl API periodically.

Polling is enabled by default every 5 seconds, since server-side applications typically run for long periods of time until the next deployment:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    // Change the default polling interval from 5 to 15 seconds
    .pollingIntervalMs(15_000)
    .build());

Polling can be started or changed at any time after the client has been created:

client.startPolling(20_000); // Every 20 seconds

You can disable polling at any time:

client.stopPolling();
// Or equivalently
client.startPolling(0);

To disable polling entirely from the start, set pollingIntervalMs to 0 in the options:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .pollingIntervalMs(0)
    .build());

You can manually trigger a refresh of the configuration at any time:

client.refetch().join();

Storages

Storages are used to cache the flag configuration between application restarts to improve startup time and provide a fallback mechanism in case the Tggl API is unreachable. They are completely optional, but highly recommended for production applications.

A storage must implement the TgglStorage interface:

import io.tggl.storage.TgglStorage;
import java.util.concurrent.CompletableFuture;
 
public class MyStorage implements TgglStorage {
 
    @Override
    public CompletableFuture<String> get() {
        // Read from your persistence layer
        return CompletableFuture.completedFuture(
            readFromDatabase()
        );
    }
 
    @Override
    public CompletableFuture<Void> set(String value) {
        // Write to your persistence layer
        writeToDatabase(value);
        return CompletableFuture.completedFuture(null);
    }
 
    @Override
    public CompletableFuture<Void> close() {
        // Optional: clean up resources
        return CompletableFuture.completedFuture(null);
    }
}

The get and set methods are used to retrieve and store the serialized configuration respectively. The serialization is handled by the client internally. The close method is optional and is called when the client is shut down.

Then pass your storage when creating the client:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .addStorage(new MyStorage())
    .build());

For redundancy, you can provide multiple storages. The client will try to read from each storage in order when starting, and will write to all storages when the configuration is updated. If one storage fails, the client can still retrieve the configuration from another.

Storage-only mode (no network)

Even if the client will prime its state from storages at startup, it will still try to fetch the latest configuration from the Tggl API unless you disable the initial fetch:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .pollingIntervalMs(0)
    .initialFetch(false)
    .addStorage(myStorage)
    .build());

This can be useful for unit tests or specific deployment scenarios where you want to use a fixed configuration without making any network request.

Shared storages

If multiple instances of your backend share the same storage (e.g. a database or cache), they will work together to keep the state up-to-date. If a single instance loses connectivity to the Tggl API, it will still be able to get updates from other instances through the shared storage.

Reporting

By default, the SDK sends flag evaluation data to Tggl to give you insights on how your flags are actually used in production and to help you manage flag lifecycles.

Reporting is batched and sent every 10 seconds by default to minimize network usage. You can change the reporting interval when creating the client by providing a custom TgglReporting instance:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .reporting(new TgglReporting(TgglReportingOptions.builder()
        .apiKey("YOUR_SERVER_API_KEY")
        .flushIntervalMs(15_000) // Every 15 seconds
        .build()))
    .build());

You can also change the reporting interval at any time after the client has been created:

TgglReporting reporting = client.getReporting();
 
// Start (or change interval) to flush every 20 seconds
reporting.start(20_000);

You can stop reporting at any time:

reporting.stop();
// Or equivalently
reporting.start(0);

You can disable reporting entirely from the beginning:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .reportingEnabled(false)
    .build());
 
TgglReporting reporting = client.getReporting();
reporting.isActive(); // false

Even when the scheduler is disabled, you can still manually flush pending reports:

reporting.flush().join();

Using the proxy

Having a proxy between your application and the Tggl API can help reduce latency, improve reliability, and reduce costs. You can configure the client to use a proxy by setting the baseUrls option:

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .baseUrls(List.of("https://my-tggl-proxy.example.com"))
    .build());

Note that baseUrls is a list, so you can provide multiple URLs for redundancy. The client will try each URL in order in case of failures. The Tggl API (https://api.tggl.io) is always added as a final fallback automatically, even if you don't include it in the list.

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    .baseUrls(List.of(
        "https://my-tggl-proxy1.example.com",
        "https://api.tggl.io",
        "https://my-tggl-proxy2.example.com"
    ))
    .build());

The baseUrls you provide are automatically passed down to the TgglReporting instance used by the client, so reporting will also go through the proxy unless you explicitly override it with a custom TgglReporting.

Retry mechanism

Because network requests can fail for various reasons, the client has a built-in retry mechanism with exponential backoff (100ms, 200ms, 400ms, capped at 500ms):

TgglClient client = new TgglClient(TgglClientOptions.builder()
    .apiKey("YOUR_SERVER_API_KEY")
    // Retry at most 2 times after first failure (default is 3)
    .maxRetries(2)
    // Timeout requests after 5 seconds (default is 8 seconds)
    .timeoutMs(5_000)
    .build());

You can disable the retry mechanism by setting maxRetries to 0, but this is not recommended in production environments. Timeouts are applied to each individual request, not to the whole retry sequence.

Info

Not every request is retried. For example, if the server returns a 401 error code, the request is considered failed and will not be retried.

The retry mechanism is applied to each URL in baseUrls individually. For example, if you have 3 URLs and maxRetries is set to 2, the client will try each URL up to 3 times (1 initial try + 2 retries) before moving to the next URL.

Tracking

To integrate with your existing tracking or analytics system, you can register a callback that will be called every time a flag is evaluated:

client.onFlagEval(event -> {
    System.out.println("Flag evaluated: " +
        event.slug() + " = " + event.value() +
        " (default: " + event.defaultValue() + ")");
});

On a per-request basis using the static client:

TgglStaticClient requestClient = client.createClientForContext(context);
 
requestClient.onFlagEval(event -> {
    System.out.println("Flag evaluated for user: " + event.slug());
});

Both onFlagEval methods return a Runnable that can be called to unregister the listener:

Runnable unsubscribe = client.onFlagEval(event -> { /* ... */ });
 
// Later, stop listening
unsubscribe.run();

Listening for config changes

You can be notified when the flag configuration changes after a fetch:

client.onConfigChange(changedFlags -> {
    System.out.println("Changed flags: " + changedFlags);
});

The callback receives a list of flag slugs that have changed (added, removed, or modified). Similarly, you can listen for successful fetches:

client.onFetchSuccessful(() -> {
    System.out.println("Config fetched successfully");
});

Error handling

For resiliency, the client swallows all errors and returns the default value when an error occurs during flag evaluation. However, you can listen to error events to be notified when something goes wrong:

client.onError(error -> {
    System.err.println("Tggl client error: " + error.getMessage());
});

Additionally, you can get the error from the last fetch operation:

Exception error = client.getError();

Gracefully shutting down

The TgglClient implements AutoCloseable, so it can be used with try-with-resources. When closed, it stops polling, flushes any pending reporting data, closes all storages, and releases all resources:

try (TgglClient client = new TgglClient(options)) {
    client.waitReady().join();
    // Use the client...
}
// All resources cleaned up automatically

Or close it manually:

client.close();

You should make sure to close the client before your application exits to ensure that the last batch of reporting data is sent.

Configuration reference

The full list of options available on TgglClientOptions.builder():

OptionTypeDefaultDescription
apiKeyStringnullYour server API key
baseUrlsList<String>[]Base URLs for the API (Tggl API is always added as fallback)
maxRetriesint3Max retry count per URL on failure
timeoutMslong8000HTTP request timeout in milliseconds
pollingIntervalMslong5000Polling interval in ms (0 to disable)
storagesList<TgglStorage>[]Storage backends for caching config
reportingTgglReportingnullCustom reporting instance (null uses the built-in one)
reportingEnabledbooleantrueEnable or disable the built-in reporting
appNameStringnullApp name sent with reports for identification
initialFetchbooleantrueWhether to fetch config on startup