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-Keyheader), 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 /livezon 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 printsreason,remainingandX-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.
Before you start
Section titled “Before you start”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.
The code
Section titled “The code”// 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.javaimport 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.
Fail open or fail closed
Section titled “Fail open or fail closed”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.
In your own service
Section titled “In your own service”- Keep one
HttpClientfor 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, waitRetry-Afterseconds, then retry with the same idempotency key. See Errors.