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.
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>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.
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 secondsYou 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(); // falseEven 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.
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 automaticallyOr 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():
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | String | null | Your server API key |
baseUrls | List<String> | [] | Base URLs for the API (Tggl API is always added as fallback) |
maxRetries | int | 3 | Max retry count per URL on failure |
timeoutMs | long | 8000 | HTTP request timeout in milliseconds |
pollingIntervalMs | long | 5000 | Polling interval in ms (0 to disable) |
storages | List<TgglStorage> | [] | Storage backends for caching config |
reporting | TgglReporting | null | Custom reporting instance (null uses the built-in one) |
reportingEnabled | boolean | true | Enable or disable the built-in reporting |
appName | String | null | App name sent with reports for identification |
initialFetch | boolean | true | Whether to fetch config on startup |