Java
The Shipeasy Java server SDK — AutoCloseable client, servlet anon filter, local evaluation, configs, experiments, and tracking.
Server-side flags, configs, experiments, and tracking for the JVM. Evaluates locally against a background-polled blob — see the shared evaluation model.
Install
<dependency>
<groupId>ai.shipeasy</groupId>
<artifactId>shipeasy</artifactId>
<version>0.1.0</version>
</dependency>Configure
Call Shipeasy.configure() once at startup with the server key, plus an optional attributes transform that maps your user object onto the Shipeasy attribute map. A simple Shipeasy.configure(apiKey) form is also available when you don't need a transform.
import ai.shipeasy.Shipeasy;
import ai.shipeasy.Client;
import ai.shipeasy.ExperimentResult;
import java.util.Map;
Shipeasy.configure(
Shipeasy.options(System.getenv("SHIPEASY_SERVER_KEY"))
.attributes(u -> Map.of("user_id", u.id(), "plan", u.plan())));Bind a client to the user
Construct a lightweight Client from your user object — the attributes transform turns it into the evaluation context, so the getters take no user argument:
Client flags = new Client(currentUser);Evaluate a feature flag
boolean enabled = flags.getFlag("new_checkout");A default overload returns the fallback only when the value can't be resolved (client not ready or gate absent) — a gate that evaluates false returns false:
boolean enabled = flags.getFlag("new_checkout", true);For the value plus the reason, use getFlagDetail — see Evaluation reasons:
FlagDetail d = flags.getFlagDetail("new_checkout");
d.value(); // boolean
d.reason(); // one of the FlagDetail.* constantsRead a dynamic config
Object cfg = flags.getConfig("billing_copy");
Object copy = flags.getConfig("billing_copy", Map.of("title", "Default"));Resolve an experiment
ExperimentResult r = flags.getExperiment("checkout_button", Map.of("color", "blue"));
if (r.inExperiment) {
r.group(); // "treatment"
r.params(); // {color=...}
}Track an event
flags.track("purchase", Map.of("amount", 49));Anonymous visitors
AnonIdFilter is a servlet Filter that mints the shared __se_anon_id cookie so logged-out traffic buckets like the browser:
@Bean
FilterRegistrationBean<AnonIdFilter> shipeasyAnonId() {
var reg = new FilterRegistrationBean<>(new AnonIdFilter());
reg.addUrlPatterns("/*");
return reg;
}
new Client(anonUser).getFlag("new_checkout"); // buckets on the cookiejakarta.servlet-api is a provided dependency. Non-servlet stacks (Ktor, Javalin) use the AnonId primitives directly.
Testing
Engine.forTesting() does zero network — no key, init/track are no-ops. Seed with override* and reset with clearOverrides. Engine is AutoCloseable, so a try-with-resources block stops the poll on close. See Testing.
try (Engine engine = Engine.forTesting()) {
engine.overrideFlag("new_checkout", true);
engine.getFlag("new_checkout", Map.of()); // true
engine.clearOverrides();
}Errors & feedback
Report a handled exception with see() so it folds into the errors primitive with a one-sentence consequence — what feature broke and how it degraded. see() rides the same Shipeasy.configure() boot (server key); there is no separate error SDK or second key.
try {
chargeCard(order, prices);
} catch (Exception e) {
Shipeasy.see(e).causesThe("checkout").to("use cached prices")
.extras(Map.of("order_id", order.id));
}Use Shipeasy.seeViolation("large query") for a non-exception problem. Full grammar — consequence phrasing, control-flow exceptions, anti-patterns — is in Error reporting with see().
The in-app bug & feature report overlay is a standalone <script> tag you drop into your frontend — platform-agnostic, no server SDK required. See The devtools overlay.