Skip to content

Java SDK

One SmplClient is the in-code half of smplkit: resolve flags, read config, control log levels, and emit audit events through a single client. You manage the underlying resources — creating flags, configs, loggers — with the MCP tools or the Console.

Install

The version below is illustrative; pin to the latest published release from Maven Central. Requires Java 17 or later.

Gradle (Kotlin DSL):

kotlin
implementation("com.smplkit:smplkit-sdk:3.0.4")

Maven:

xml
<dependency>
    <groupId>com.smplkit</groupId>
    <artifactId>smplkit-sdk</artifactId>
    <version>3.0.4</version>
</dependency>

The JUL, Logback (SLF4J), and Log4j2 logging adapters ship inside the main artifact — no separate module to add.

Initialize the client

A single client exposes all four products as public fields: client.flags, client.config, client.logging, and client.audit. The SDK key comes from SMPLKIT_API_KEY (or the ~/.smplkit profile, or .apiKey(...)). Set environment once on the builder — flag/config/logger resolution is per-environment, and it is never a per-call argument.

SmplClient is AutoCloseable, so use try-with-resources (or call close()) to tear down the live-updates WebSocket and metrics threads.

java
import com.smplkit.SmplClient;

try (SmplClient client = SmplClient.builder()
        .environment("production")
        .service("my-service")
        .build()) {
    client.waitUntilReady();   // optional: block until live-updates are subscribed
    // ... client.flags / client.config / client.logging / client.audit
}

SmplClient.create() is the zero-config shortcut when every setting comes from the environment or ~/.smplkit. Call waitUntilReady() before firing a management write you expect to observe back over the WebSocket — without it the broadcast can race the subscribe. An AsyncSmplClient exists for asynchronous use; the synchronous client above is the common path.

Flags

Use flags for runtime decisions you want to flip without a deploy — kill switches, gradual rollouts, per-plan gating. Declare a typed handle with a safe default, attach context, and evaluate with .get(). The default is served if the flag is missing or smplkit is unreachable, so pick the conservative value.

Java attaches context ambiently via client.setContext(...) — typically called once per request from middleware — with an optional per-call override that wins over it.

java
import com.smplkit.Context;
import com.smplkit.flags.Flag;

// declare typed handles with safe defaults
Flag<Boolean> checkoutV2 = client.flags.booleanFlag("checkout-v2", false);
Flag<String> bannerColor = client.flags.stringFlag("banner-color", "red");
Flag<Number> maxRetries  = client.flags.numberFlag("max-retries", 3);

// attach ambient context for this thread (e.g. from request middleware)
client.setContext(List.of(
        Context.builder("user", "alice.adams@acme.com")
                .attr("plan", "enterprise")
                .attr("beta_tester", true)
                .build()));

// evaluate — synchronous, local, zero network after the first call
boolean checkout = checkoutV2.get();

// override context per-call (highest priority)
boolean guest = checkoutV2.get(List.of(
        Context.builder("user", "guest-1").attr("plan", "free").build()));

→ Full reference (every language, all context modes, change listeners): Flags Runtime

Config

Use config for values you want to change at runtime without redeploying — feature limits, thresholds, service endpoints. Read a single value with getValue(...) (the 3-arg form with a default never throws), or bind(...) a POJO/Map to get a live object that smplkit mutates in place as values change.

java
// single-value read with a default (returned when the key is absent)
int slowQueryMs = ((Number) client.config.getValue(
        "showcase-database", "slow_query_threshold_ms", 500)).intValue();

// bind a POJO — the returned object is live and updated in place
Billing billing = client.config.bind("showcase-billing", new Billing());
System.out.println("max seats = " + billing.plan.max_seats);

// optionally inherit defaults from a parent config
Common common = client.config.bind("showcase-common", new Common());
Billing scoped = client.config.bind("showcase-billing", new Billing(), common);

Config Runtime

Logging

Use logging to drive native log levels from the server — raise a logger to DEBUG in production to investigate, then revert, with no redeploy. One install() call discovers every logger registered with the JVM's frameworks (JUL always; SLF4J–Logback and Log4j2 when on the classpath, auto-loaded via ServiceLoader), registers them, and applies server-managed levels. Use your loggers normally afterward.

java
try (SmplClient client = SmplClient.create()) {
    client.logging.install();
    System.out.println("All loggers are now controlled by smplkit");
}

To support a framework outside the three built-ins, call client.logging.registerAdapter(myAdapter) before install().

Logging Runtime

Audit

Use audit to record an immutable trail of consequential actions — invoices issued, permissions changed, data exported. client.audit.events().record(...) is fire-and-forget (buffered and retried); call flush(...) when you need the event durable before reading it back or exiting. event_type, resource_type, and resource_id are required, and resource_type must not start with smpl. (that prefix is reserved).

java
import com.smplkit.audit.CreateEventInput;

CreateEventInput input = new CreateEventInput("invoice.created", "invoice", resourceId);
input.actorLabel = "finance@example.com";
input.category = "billing";
input.severity = "WARN";
input.data = Map.of("snapshot", Map.of("total_cents", 4900, "currency", "USD"));
client.audit.events().record(input);

// flush so the event is durable before you read it back or exit
client.audit.events().flush(5_000);

Emit audit events

The author + operate loop

The code declares the resource; the MCP tools provision and verify it. Write the SDK call, create the resource with create_flag / create_config / set_log_level, then confirm with get_flag / get_config / query_events. The id used in code must equal the key provisioned by the tool.

Example — a kill switch for the new checkout. In code:

java
Flag<Boolean> checkoutV2 = client.flags.booleanFlag("checkout-v2", false);
if (checkoutV2.get()) {
    renderNewCheckout();
}

Then provision with create_flag(key="checkout-v2", type="boolean", default=false) and verify with get_flag(key="checkout-v2"). The handle id "checkout-v2" in code is the same key you provisioned.