Server SDKs

Java Server SDK

Introduction

The Java Server SDK is a server-side SDK that evaluates configs and their targeting rules. Upon initialization, it retrieves configs and targeting rules from ConfigDirector services. From that point on, it evaluates configs locally for any user context, and receives updates via server sent events (SSE) when configs are updated on the dashboard or via the admin API.

The minimum Java version supported is 17.

The client is thread safe. Create one instance when your application starts, share it for the lifetime of the process, and close it on shutdown. Evaluations read config state the client already holds in memory, so they make no network calls on the request path.

Installation

The SDK can be installed from Maven Central: https://central.sonatype.com/artifact/com.configdirector/configdirector-server-sdk

dependencies {
    implementation("com.configdirector:configdirector-server-sdk:0.1.0")
}

Configure and initialize the client

  1. Create an instance of the client providing your server SDK key. You can retrieve a server SDK key under your project settings in the Environments & SDK Keys tab.
  2. Initialize the client to initiate its connection lifecycle.
ConfigDirectorSetup.java
import com.configdirector.ConfigDirector;
import com.configdirector.ConfigDirectorClient;

// IMPORTANT: Do not commit the server SDK key to your source code, it is a secret value.
// Building the client makes no network calls.
ConfigDirectorClient client = ConfigDirector.client("YOUR-SERVER-SDK-KEY");

// Blocks until the client is initialized or it times out.
// If initialization times out, the client will continue attempting to
// initialize in the background.
client.initialize();

The client is thread safe. Create one at startup, share it for the lifetime of the process, and call close on shutdown.

Server SDK keys are secret values. Do not commit them to your source code repository. Provide them at runtime via environment variables instead.

initialize blocks until the initial config state arrives or the configured timeout elapses, and it never throws on a connection failure. Check isReady to find out whether config state actually arrived. An overload accepts a Duration to override the configured timeout for that call:

ConfigDirectorSetup.java
client.initialize(Duration.ofSeconds(5));

Additional configuration options

Options are adjusted through the lambda passed to ConfigDirector.client. Every setter returns the options object, so calls chain. They are read once when the client is built; changing them afterwards has no effect.

metadata

The metadata option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application.

ConfigDirectorSetup.java
import com.configdirector.ConfigDirector;
import com.configdirector.ConfigDirectorClient;

ConfigDirectorClient client = ConfigDirector.client(
    "YOUR-SERVER-SDK-KEY",
    options -> options.metadata("YOUR-APP-NAME", "1.0.2"));

client.initialize();

logger

The SDK logs through SLF4J. Left to itself it writes to the logger named com.configdirector, so logging levels, formatting, and destinations can be controlled through your usual SLF4J configuration:

logback.xml
<logger name="com.configdirector" level="DEBUG" />

Alternatively, pass any SLF4J Logger to the logger option to put the SDK's output under your application's own logging namespace, where existing appenders and level configuration already apply:

ConfigDirectorSetup.java
import org.slf4j.LoggerFactory;

ConfigDirectorClient client = ConfigDirector.client(
    "YOUR-SERVER-SDK-KEY",
    options -> options.logger(LoggerFactory.getLogger("my-app.configdirector")));

connection

The connection option accepts a lambda receiving a builder with the following optional values:

  • mode
    • The connection mode, which can be ConnectionMode.STREAMING, ConnectionMode.POLLING, or ConnectionMode.ONE_TIME. It is recommended to use the default of STREAMING unless you have a specific need to use one of the others instead. ONE_TIME retrieves config state during initialization only, and never refreshes it.
    • Defaults to ConnectionMode.STREAMING
  • pollingInterval
    • Only used in POLLING mode. The interval to poll ConfigDirector services for updates.
    • Defaults to 60 seconds
  • timeout
    • The timeout to be used in initialization. This is how long the initialize method will wait for data from ConfigDirector services. If the timeout is reached, initialize will return but the client will still be in an unready status and returning default values. While streaming, the client will continue to attempt to connect and retrieve config values in the background.
    • Defaults to 3 seconds
  • url
    • The base URL used to connect to ConfigDirector services
    • This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services
ConfigDirectorSetup.java
import com.configdirector.ConfigDirector;
import com.configdirector.ConnectionMode;
import java.time.Duration;

ConfigDirectorClient client = ConfigDirector.client(
    "YOUR-SERVER-SDK-KEY",
    options -> options.connection(connection -> connection
        .mode(ConnectionMode.POLLING)
        .pollingInterval(Duration.ofSeconds(30))
        .timeout(Duration.ofSeconds(5))));

Connection settings can also be built once with ConnectionOptions.builder() and shared by several clients:

ConfigDirectorSetup.java
import com.configdirector.ConnectionOptions;

ConnectionOptions connection = ConnectionOptions.builder()
    .mode(ConnectionMode.POLLING)
    .pollingInterval(Duration.ofSeconds(30))
    .build();

ConfigDirectorClient client = ConfigDirector.client(
    "YOUR-SERVER-SDK-KEY",
    options -> options.connection(connection));

telemetry

Telemetry tuning. It is unlikely these settings need to be adjusted. However, in cases where your application has a large number of evaluations per second, you can adjust these settings to tune the memory footprint and frequency of telemetry requests.

Keep in mind that ConfigDirector relies on these telemetry events to provide insights and features related to the configs being used.

The telemetry builder accepts the following optional values:

  • eventQueueLimit
    • The size limit of telemetry event queues. If the size limit is reached before the events are flushed to the network, older events will be dropped.
    • ConfigDirector keeps a count of dropped events. If the number of dropped events is higher than 50% of the total events, ConfigDirector will issue a notification alert in the dashboard.
    • A number between 100 and 100,000. Defaults to 5,000.
  • flushInterval
    • How often events are flushed and sent over the network.
    • Decrease this number if your application consistently captures a large number of events in short periods of time in order to reduce memory footprint from a large event queue.
    • Defaults to 30 seconds.
ConfigDirectorSetup.java
import java.time.Duration;

ConfigDirectorClient client = ConfigDirector.client(
    "YOUR-SERVER-SDK-KEY",
    options -> options.telemetry(telemetry -> telemetry
        .eventQueueLimit(10_000)
        .flushInterval(Duration.ofSeconds(15))));

A value outside the accepted range throws a ConfigDirectorValidationException when the client is built.

Hooks

Hooks are events you can subscribe to in order to be notified of some key actions from the client. Each registration method takes a handler and returns a Subscription, which cancels the registration when closed:

ConfigDirectorSetup.java
Subscription subscription = client.onConfigsUpdated(event -> System.out.println(event.keys()));

subscription.close(); // Cancels the registration

The following hooks are available:

  • onClientReady(Consumer<ClientReadyEvent>)
    • Emitted when the client is initialized. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs. It is emitted once, and a handler registered after that point is never called.
  • onConfigsUpdated(Consumer<ConfigsUpdatedEvent>)
    • Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization when an entire config payload is received. After initialization, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used).
    • keys(): List<String> - The config keys that were included in the payload from the server.
    • Handlers run on the transport thread, so one that blocks delays later updates.
  • onConfigEvaluated(Consumer<ConfigEvaluatedEvent>)
    • Emitted whenever a config is evaluated. This includes calls to the getters and evaluations delivered to watchers via watch.
    • evaluation(): ConfigEvaluation - A ConfigEvaluation record containing the details of the evaluation:
      • key(): String - The config key that was evaluated
      • value(): Object - The value the config evaluated to, in the type the caller's default asked for. It can be the default value provided to the getter or to watch. For example, if the config was evaluated before the client was initialized.
      • valueId(): String - The value ID, which is a stable hash of the value that can be used for analytics or other third parties rather than sending the value. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves to third parties.
      • isDefault(): boolean - Whether or not the evaluation fell back to the default value provided in code.
      • reason(): EvaluationReason - The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will be FOUND_MATCH. If the evaluation had to fall back to the default value, the reason will encode why the fallback was required:
        • CLIENT_NOT_READY - The evaluation happened before the client finished initialization
        • CONFIG_STATE_MISSING - The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs.
        • INVALID_NUMBER - The config was requested as a number but the value received from the server could not be converted to a number.
        • INVALID_BOOLEAN - The config was requested as a boolean but the value received from the server could not be converted to a boolean.
        • INVALID_JSON - The config was requested as JSON but the value received from the server could not be converted to a JSON object or array.
        • VALUE_MISSING - The config key was present in the payload received from the server, but it carried no value.
      • context(): Context - The user context that was provided to the evaluation function, or null if no context was provided.
    • Handlers run on the calling thread, so one that blocks delays the getter that triggered it.

Retrieve config values

Retrieve config values via the typed getters, and subscribe to updates via watch. The first argument is the config key and the second is the default value, which is returned if the config is not available or cannot be parsed as the given type:

Main.java
import com.configdirector.Subscription;
import java.util.Map;

// Retrieve config values
int retries = client.getInteger("max-retries", 3);
String theme = client.getString("theme", "light");
boolean newCheckout = client.getBoolean("new-checkout", false);
Map<String, Object> limits = client.getJsonObject("rate-limits", Map.of("per_minute", 60)); // JSON config

// Watch for config updates
Subscription subscription = client.watch(
    "new-checkout",
    false,
    value -> System.out.println("new-checkout is now " + value));

// Call close when the subscription is no longer needed
subscription.close();

The type a config is parsed as is decided by the getter you call. Each one takes the value to return when the config is missing, the service is unreachable, or the value will not convert to the requested type — so the default should always be the safe choice:

  • getBoolean(String configKey, boolean defaultValue)
  • getString(String configKey, String defaultValue)
  • getInteger(String configKey, int defaultValue)
  • getDouble(String configKey, double defaultValue)
  • getJsonObject(String configKey, Map<String, Object> defaultValue)
  • getJsonArray(String configKey, List<Object> defaultValue)

There is also getValue, the counterpart to getValue in the other ConfigDirector SDKs. It takes the type from the default value, which must be a Boolean, String, Integer, Long, Double, Float, Map, or List, and must not be null:

Main.java
boolean newCheckout = client.getValue("new-checkout", false);
String theme = client.getValue("theme", "light");
Getters never throw when a config is missing or a value will not convert — they return your default instead. Which of those happened is reported through onConfigEvaluated.

Evaluate config values with a user context

Unlike client SDKs, the server SDKs are able to evaluate targeting rules for the given user context locally without additional network calls.

Every getter has an overload taking a Context as its last argument, which is what targeting rules are evaluated against. The same key can therefore resolve differently per user:

Main.java
import com.configdirector.Context;

Context context = Context.builder()
    .id("user-id")
    .name("Example User")
    .trait("region", "North America")
    .build();

client.getBoolean("my-boolean-config-key", false, context);

Context is built with Context.builder() and accepts the following:

  • id
    • The user's identifier. It decides their bucket in a percentage rollout, so changing it can move a user into a different percentile. If it is not provided, a percentage rollout assigns an unstable bucket.
  • name
    • The user's display name.
  • trait(String key, Object value) / traits(Map<String, Object> traits)
    • Arbitrary traits which can be referenced in targeting rules. Values are JSON-shaped: String, Number, Boolean, List, Map, or null. Anything else has no text form and will not match a targeting rule.
    • trait adds a single trait, keeping the rest. traits replaces every trait set so far.
  • anonymous
    • Keeps the context out of the dashboard: it is evaluated but never persisted, and telemetry reports neither the context nor its id.

watch accepts four arguments, the first is the config key, the second argument is the default value, the third argument is a handler that will be executed when the config value is updated, and the fourth and optional argument is a user context:

Main.java
import com.configdirector.Context;
import com.configdirector.Subscription;

Subscription subscription = client.watch(
    "new-checkout",
    false,
    value -> System.out.println("new-checkout is now " + value),
    Context.builder()
        .id("user-id")
        .name("Example User")
        .trait("region", "North America")
        .build());

Other useful client features

The client provides additional methods.

isReady

Returns a boolean indicating if the client has received config state and is ready to evaluate configs. It is initially false and becomes true once the first config state arrives. Until it does, every getter returns its default.

isClosed

Returns a boolean indicating if close has been called. A closed client cannot be reopened.

getAllConfigs

Returns every config the client currently holds as a Map<String, ConfigState>, evaluated but before type parsing. It optionally accepts a Context and a list of config keys to restrict the result to.

This is intended for handing state to a client SDK to hydrate with. It records no telemetry, since the SDK that receives the state reports its own evaluations.

unwatch

Cancels every watch on one config key.

unwatchAll

Cancels every watch on every config key.

close

Closes all connections to ConfigDirector services, reports whatever telemetry is pending, and cancels every watch and event handler. Calling it twice is harmless.

Only call close when your application shuts down and it will no longer make use of the client instance. ConfigDirectorClient implements AutoCloseable, so in a container such as Spring it is enough to let the container own the lifecycle:

ConfigDirectorConfiguration.java
@Bean(destroyMethod = "close")
public ConfigDirectorClient configDirectorClient() {
  ConfigDirectorClient client = ConfigDirector.client(System.getenv("CONFIGDIRECTOR_SERVER_KEY"));
  client.initialize();
  return client;
}
Copyright © 2026