Java
Java client for Switchyard feature flags. Java 17+, no dependencies.
SwitchyardClient flags = SwitchyardClient.builder(System.getenv("SWITCHYARD_SDK_KEY"))
.baseUri("https://switchyard.example.com")
.build();
// Optional: wait briefly for the first fetch. Checks work straight away, returning defaults until then.
flags.awaitReady(Duration.ofSeconds(2));
if (flags.isEnabled("new-checkout", false)) {
// new code path
}Create one client per SDK key and share it: it's thread-safe. Close it on shutdown (try-with-resources or your framework's lifecycle).
Flags with variations
Flags that serve text, numbers or JSON instead of on/off have typed methods. Each returns the default before the first fetch, for unknown flags, and for flags of another type:
String color = flags.stringVariation("checkout-button", user, "green");
double limit = flags.numberVariation("upload-limit-mb", user, 10);
Object plan = flags.jsonVariation("plan-config", user, Map.of()); // a Map, List, String, number, Boolean or nullSegments and prerequisites arrive with the flags and are evaluated in memory too. A context without a key can still carry attributes: EvaluationContext.anonymousBuilder().attribute("country", "IN").build().
For OpenFeature, see switchyard-openfeature. Evaluation.evaluate(snapshot, flagKey, context) gives a flag's value with the reason for it, outside a client.
Use it in your app
It isn't on Maven Central yet, so build it into your local Maven repository first:
cd clients/java
./mvnw install -DskipTestsThen depend on it:
<dependency>
<groupId>io.github.nikolaviews</groupId>
<artifactId>switchyard-java</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>With Gradle, add mavenLocal() to your repositories and implementation("io.github.nikolaviews:switchyard-java:0.1.0-SNAPSHOT").
Pass the SDK key through configuration (an environment variable or a secrets manager), never in source code. Each environment (development, production) has its own key.
Targeting and rollouts
Flags with targeting rules or percentage rollouts need to know who the check is for:
EvaluationContext user = EvaluationContext.builder(userId) // a stable ID: same user, same key
.attribute("email", user.email().toLowerCase())
.attribute("country", user.country())
.build();
if (flags.isEnabled("new-checkout", user, false)) { ... }- Rules are matched in order, all in memory; the first whose clauses all match decides. Otherwise the fallthrough decides.
- Percentage rollouts hash the flag key with the context key (MurmurHash3), so a user's answer is stable across restarts and machines, and raising the percentage only ever adds users.
- Comparisons are case-sensitive, so lowercase attributes like email where case shouldn't matter.
isEnabled(key, default)checks for an anonymous context: fine for plain on/off flags, but rules never match it and it's only in a rollout at 100%.
The exact rules, shared by every Switchyard library, are in spec/EVALUATION.md; the shared test vectors run as part of this library's tests.
How it behaves
| Situation | What isEnabled(key, context, default) returns |
|---|---|
| Before the first fetch | default |
| Flag switched off | false (off overrides every rule) |
| Flag on | the first matching rule's result, else the fallthrough's, for this context |
| Flag unknown (never created, archived, misspelled) | default |
| Server down, slow, or returning errors | the last values it received; retries with exponential backoff and jitter |
| SDK key rejected (rotated or revoked) | the last values it received |
After close() |
the last values it received; no more polling |
- Fast checks:
isEnabledreads an immutable in-memory map. It never blocks, never calls the network and never throws (about 4 ns per check in a quick local loop; a JMH benchmark comes in phase 6). - Live updates: a Server-Sent Events stream tells the client about each change, and it fetches the new flags straight away: about a second end to end, and under 600 ms for 1,000 clients at once in our load test. While the stream is up, polling slows to every 5 minutes as a safety net. If the stream drops, the client catches up immediately, polls at the normal rate and reconnects with backoff. A stream that goes quiet for 75 seconds (three missed heartbeats) is treated as dead. Servers without streams (before phase 4) just get polled.
- Cheap polling: every poll sends the last
ETag, so an unchanged environment costs a bodiless304. - Backoff: the normal interval while healthy, doubling per consecutive failure up to
maxBackoff, with ±20% jitter so clients don't all retry together. - Status:
status()returnsSTARTING,READY,STALE,UNAUTHORIZEDorCLOSED;isStreaming()says whether the live-update stream is connected. - Changes:
addChangeListener(keys -> ...)is called with the keys of flags that were added, removed or changed. - Logging: through
System.Logger(loggerio.github.nikolaviews.switchyard.client.SwitchyardClient), which SLF4J and Log4j can bridge. It warns once per outage, not on every retry.
Options
| Builder method | Default | |
|---|---|---|
baseUri |
http://localhost:8080 |
The Switchyard server |
pollInterval |
30 s | How often to check for changes while not streaming (minimum 100 ms); every 5 min while streaming |
requestTimeout |
10 s | Connect and response timeout per request |
maxBackoff |
5 min | Longest wait between retries while failing |
httpClient |
a new HttpClient |
Bring your own, e.g. for a proxy or custom TLS |
streaming |
true |
Listen for changes over Server-Sent Events; false to rely on polling alone |
Build
./mvnw verify