Java SDK
The ReleaseAnchor Java SDK requires Java 21 and works in any JVM application — Spring Boot, Quarkus, Micronaut, or plain Java.
📦 Maven Central: repo1.maven.org
Installation
Maven
<dependency>
<groupId>com.releaseanchor</groupId>
<artifactId>release-anchor-java</artifactId>
<version>1.0.0</version>
</dependency>Gradle
implementation "com.releaseanchor:release-anchor-java:1.0.0"Available on Maven Central — no additional repository configuration required.
First integration
Before you write SDK code, set up the product side in the dashboard:
- Sign in to Release Anchor
- Create a project and its environments
- Create an environment API key
- Create the flag you want to evaluate
If you plan to use Smart Insights, enable it for the flag in the dashboard before integrating the feedback helpers below. See Smart Insights →.
Quick start
import com.releaseanchor.sdk.ReleaseAnchor;
import com.releaseanchor.sdk.EvaluateResponse;
ReleaseAnchor client = ReleaseAnchor.builder()
.apiKey(System.getenv("RELEASE_ANCHOR_KEY"))
.build();
EvaluateResponse result = client.evaluate("dark-mode", "user-123").join();
if (result.value()) {
// feature is on for this user
}evaluate() returns CompletableFuture<EvaluateResponse>. Use .join() to block, or chain with .thenCompose() for non-blocking code.
EvaluateResponse is a Java record with accessors:
value()— the boolean result you should use to gate your featurematchedRuleType()—STATIC | SEGMENT | PERCENTAGE | nullerror()— populated on technical failures,nullon successevaluationId()— present when Smart Insights feedback is enabled for the flag
Primary API surface
Most integrations only need:
evaluate(flagKey, userId)— async, returnsCompletableFuture<EvaluateResponse>evaluateSync(flagKey, userId)— blocking, returnsbooleanevaluateBulk(flagKey, userIds)— async bulkevaluateBulkSync(flagKey, userIds)— blocking bulk
Configuration
ReleaseAnchor client = ReleaseAnchor.builder()
.apiKey("<YOUR_API_KEY>") // Required - get from the API Keys page
.apiVersion("v1") // "v1" | "v2"
.baseUrl("https://...") // Override API base URL
.timeout(Duration.ofSeconds(5)) // Request timeout
.defaultValue(false) // Fallback value on technical errors
.strict4xx(false) // Throw ReleaseAnchorException on unexpected 4xx
.logger((msg, ctx) -> log.warn(msg, ctx)) // Called on technical errors
.build();| Option | Type | Description |
|---|---|---|
apiKey | String | Required. Your environment API key. |
apiVersion | String | API version used by the client. |
baseUrl | String | Override the API base URL. |
timeout | Duration | Per-request timeout. Default: 5 seconds. |
defaultValue | boolean | Returned on technical failures such as network or timeout errors. |
strict4xx | boolean | Throws ReleaseAnchorException on unexpected 4xx responses instead of silently falling back. |
logger | BiConsumer<String, Object> | Called on technical errors for observability. Default: writes to stderr. |
evaluate(flagKey, userId)
Evaluates a single flag for a user. Concurrent calls for the same flagKey + userId + defaultValue are deduplicated while the request is in flight.
// Async
client.evaluate("dark-mode", "user-123")
.thenAccept(result -> {
if (result.value()) serveNewFeature();
});
// Blocking
EvaluateResponse result = client.evaluate("dark-mode", "user-123").join();
// Blocking shorthand (returns boolean directly)
boolean on = client.evaluateSync("dark-mode", "user-123");
// Per-call defaultValue override
boolean on = client.evaluateSync("dark-mode", "user-123", true);evaluateBulk(flagKey, userIds)
Evaluates one flag for multiple users in a single request.
List<String> users = List.of("user-1", "user-2", "user-3");
Map<String, EvaluateResponse> results = client.evaluateBulkSync("dark-mode", users);
for (Map.Entry<String, EvaluateResponse> entry : results.entrySet()) {
if (entry.getValue().value()) {
System.out.println(entry.getKey() + ": feature on");
}
}Missing keys in the server response are filled with a fallback entry. Extra keys are ignored.
Smart Insights
Use the Smart Insights helpers when you want the SDK to evaluate a flag and also report execution outcomes.
Smart Insights connects evaluation events with real execution results such as success, failure, and latency. Use it when you want feedback data in the dashboard instead of evaluation-only visibility.
executeWithFeedback(flagKey, userId, handler)
Single-user helper that:
- calls
evaluate(...)first - runs your handler with the evaluation result
- calls
reportSuccess(...)orreportFailure(...)automatically whenevaluationIdis present
boolean result = client.executeWithFeedback(
"checkout-redesign",
"user-123",
evaluation -> CompletableFuture.supplyAsync(() -> {
if (!evaluation.value()) return false;
return runCheckoutExperience();
})
).join();Handler semantics:
- return
true→ reports success - return
false→ reports failure withEXECUTION_FAILED - throw → reports failure with
UNKNOWNand rethrows the original error
executeWithFeedback(flagKey, userIds, handler)
Bulk helper that:
- calls
evaluateBulk(...)first - runs the handler independently for each user
- sends one bulk feedback request after processing all users with
evaluationId
List<String> users = List.of("user-1", "user-2", "user-3");
Map<String, Boolean> results = client.executeWithFeedback(
"checkout-redesign",
users,
(userId, evaluation) -> CompletableFuture.supplyAsync(() -> {
if (!evaluation.value()) return false;
return runExperienceForUser(userId);
})
).join();Handler errors for one user do not stop processing for the rest.
Manual reporting
Use these methods when you want to report execution results manually instead of relying on executeWithFeedback.
reportSuccess(evaluation)
Reports a successful execution outcome. If evaluation.evaluationId() is null, the call is a no-op. Never throws.
EvaluateResponse evaluation = client.evaluate("checkout-redesign", "user-123").join();
// without latency
client.reportSuccess(evaluation);
// with latency
client.reportSuccess(evaluation, 125L);reportFailure(evaluation)
Reports a failed execution outcome. If evaluation.evaluationId() is null, the call is a no-op. Never throws.
EvaluateResponse evaluation = client.evaluate("checkout-redesign", "user-123").join();
// default errorType: UNKNOWN
client.reportFailure(evaluation);
// with errorType and latency
client.reportFailure(evaluation, "EXECUTION_FAILED", 125L);Error handling
Network failures, timeouts, 401, 429, and 5xx responses are caught internally, logged, and returned as fallback responses. They never throw.
EvaluateResponse result = client.evaluate("dark-mode", "user-123").join();
if (result.error() != null) {
// result.error().type(): "NETWORK_ERROR" | "TIMEOUT" | "UNAUTHORIZED"
// "RATE_LIMITED" | "HTTP_ERROR" | "PARSE_ERROR"
// result.error().message(): String
}reportSuccess(), reportFailure(), and the feedback side of executeWithFeedback() are best-effort — they swallow network and HTTP failures so feedback delivery never breaks your application flow.
strict4xx
Set .strict4xx(true) to throw ReleaseAnchorException on unexpected 4xx responses instead of silently falling back. Useful for catching integration issues during development.
import com.releaseanchor.sdk.ReleaseAnchorException;
ReleaseAnchor client = ReleaseAnchor.builder()
.apiKey("...")
.strict4xx(true)
.build();
try {
EvaluateResponse result = client.evaluate("dark-mode", "user-123").join();
} catch (ReleaseAnchorException e) {
System.err.println("Unexpected HTTP error: " + e.getStatus());
}Timeouts are still returned as fallback responses, not thrown.
Spring Boot example
@Service
public class CheckoutService {
private final ReleaseAnchor client;
public CheckoutService(@Value("${releaseanchor.api-key}") String apiKey) {
this.client = ReleaseAnchor.builder()
.apiKey(apiKey)
.build();
}
public CheckoutView getCheckout(String userId) {
boolean newFlow = client.evaluateSync("new-checkout", userId);
return newFlow ? new CheckoutV2() : new CheckoutV1();
}
}# application.yaml
releaseanchor:
api-key: ${RELEASE_ANCHOR_KEY}Cleanup
ReleaseAnchor implements AutoCloseable. Call close() or use try-with-resources during application shutdown to release the underlying HTTP client:
// try-with-resources
try (ReleaseAnchor client = ReleaseAnchor.builder().apiKey(apiKey).build()) {
// use client
}
// or explicit close
client.close();