Skip to content

Java example

There’s no Java SDK. You don’t need one: the JDK’s java.net.http.HttpClient is enough to ask Ordainer before a tool runs. This page is one self-contained file you can run with java Decide.java.

It does what our SDKs do on each call:

  • Sends an idempotency key (Idempotency-Key header), a new one per real call. A retry with the same key gets the original answer back and isn’t counted twice.
  • Uses a 300 ms timeout per attempt and retries once with the same idempotency key on a timeout, a network error or a 503, like our Node and Python SDKs.
  • Makes one warm-up request at startup (GET /livez on the API address; it needs no key, moves no usage counter and writes no audit row), so the first real call doesn’t also pay for opening the connection.
  • Runs the tool on allow, stops on a deny, and prints reason, remaining and X-Request-Id.

Every allowed call counts as 1. Extra fields such as units, quantity, cost or customer_id are ignored, so the example sends only tool.

Do steps 1 and 2 of Your first deny: you’ll have a pilot key in ORDAINER_KEY and the API address in ORDAINER_BASE_URL. You need Java 17 or later.

Decide.java
// Java 17+, JDK only (java.net.http). Plain HTTP, no SDK.
// Run after "Your first deny" step 2 (ORDAINER_KEY and ORDAINER_BASE_URL exported): java Decide.java
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Decide {
static final String BASE_URL = System.getenv("ORDAINER_BASE_URL");
static final String KEY = System.getenv("ORDAINER_KEY");
// One client for the whole process: it keeps the connection open between calls.
static final HttpClient HTTP = HttpClient.newBuilder().connectTimeout(Duration.ofMillis(300)).build();
// 300 ms per attempt, like our Node and Python SDKs. Pick a timeout that fits your request path.
static final Duration TIMEOUT = Duration.ofMillis(300);
// One answer from Ordainer. status is 0 when there was no answer (timeout or network error).
record Answer(int status, String body, String requestId) {}
static Answer send(String tool, String idempotencyKey) throws InterruptedException {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE_URL + "/v1/decide"))
.timeout(TIMEOUT)
.header("Authorization", "Bearer " + KEY)
.header("Content-Type", "application/json")
.header("Idempotency-Key", idempotencyKey)
.POST(HttpRequest.BodyPublishers.ofString("{\"tool\":\"" + tool + "\"}"))
.build();
try {
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
return new Answer(res.statusCode(), res.body(), res.headers().firstValue("X-Request-Id").orElse(""));
} catch (HttpTimeoutException e) {
return new Answer(0, "timeout", "");
} catch (IOException e) {
return new Answer(0, e.toString(), "");
}
}
// Asks once and, on no answer or a 503, retries once with the SAME idempotency key,
// so a call that reached us is answered once and counted once.
static Answer decide(String tool, String idempotencyKey) throws InterruptedException {
Answer a = send(tool, idempotencyKey);
if (a.status() == 0 || a.status() == 503) {
Thread.sleep(50);
a = send(tool, idempotencyKey);
}
return a;
}
// Warm-up at startup: opens the connection before the first real call. GET /livez needs
// no key, moves no usage counter and writes no audit row.
static void warmUp() throws InterruptedException {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE_URL + "/livez")).timeout(Duration.ofSeconds(2)).GET().build();
try {
HTTP.send(req, HttpResponse.BodyHandlers.discarding());
} catch (IOException e) {
// Nothing to do: the first real call will open the connection instead.
}
}
// A tiny reader for the fixed response shape. Use your JSON library in real code.
static String field(String json, String name) {
Matcher m = Pattern.compile("\"" + name + "\":(\"([^\"]*)\"|[^,}]*)").matcher(json);
if (!m.find()) return null;
return m.group(2) != null ? m.group(2) : m.group(1);
}
public static void main(String[] args) throws InterruptedException {
if (BASE_URL == null || KEY == null) {
System.err.println("Export ORDAINER_KEY and ORDAINER_BASE_URL first.");
System.exit(1);
}
warmUp();
// A new key for each real call. Reuse a key only to retry the same call.
Answer a = decide("research.run", "research-run-" + UUID.randomUUID());
if (a.status() != 200) {
// No answer, a 5xx, or an error answer (4xx). Branch on error.code, never on the message.
String code = a.status() == 0 ? "no_answer" : field(a.body(), "code");
System.err.println("error " + a.status() + ": " + code + " (request_id " + a.requestId() + ")");
if (a.status() == 0 || a.status() >= 500) {
// Ordainer couldn't answer. Your choice: fail open (run the tool) or fail closed (don't).
boolean failOpen = true;
if (failOpen) {
System.err.println("failing open: run the tool");
return;
}
System.err.println("failing closed: don't run the tool");
System.exit(1);
}
// A 4xx did answer: fix the key or the request, and don't run the tool.
System.exit(1);
}
String decision = field(a.body(), "decision");
String reason = field(a.body(), "reason");
String remaining = field(a.body(), "remaining");
String limit = field(a.body(), "limit");
String left = "null".equals(limit) ? "no cap" : remaining + "/" + limit + " left";
if ("allow".equals(decision)) {
System.out.println("allow: run the tool (" + left + ", request_id " + a.requestId() + ")");
} else if ("shadow".equals(field(a.body(), "mode"))) {
System.out.println("would deny (shadow): " + reason + ", run the tool anyway (" + left + ")");
} else {
System.out.println("deny: " + reason + ", don't run the tool (" + left + ", resets "
+ field(a.body(), "resets_at") + ", request_id " + a.requestId() + ")");
System.exit(2);
}
}
}

Run it 6 times with the pilot key: calls 1 to 5 print allow, and call 6 prints deny: quota_exceeded and exits 2.

When Ordainer can’t answer (a timeout, a network error or a 5xx), your code decides. Set failOpen to true to let the tool run, or false to block it. A 4xx is different: we did answer, so fix the key or the request and don’t run the tool. More in Fail open vs fail closed.

  • Keep one HttpClient for the whole process. It reuses the connection between calls.
  • Make the warm-up request once at boot, not before each call. The first call from a cold JVM can be slower than the calls after it.
  • Build idempotency keys from your own data, such as a job ID or queue message ID, one key per real operation. A key replays the first answer for 24 hours. More in Idempotency and retries.
  • Use your JSON library (Jackson, Gson) to read the response instead of the small reader here. Ignore fields you don’t know.
  • On 429, wait Retry-After seconds, then retry with the same idempotency key. See Errors.