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 feature
  • matchedRuleType()STATIC | SEGMENT | PERCENTAGE | null
  • error() — populated on technical failures, null on success
  • evaluationId() — present when Smart Insights feedback is enabled for the flag

Primary API surface

Most integrations only need:

  • evaluate(flagKey, userId) — async, returns CompletableFuture<EvaluateResponse>
  • evaluateSync(flagKey, userId) — blocking, returns boolean
  • evaluateBulk(flagKey, userIds) — async bulk
  • evaluateBulkSync(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();
OptionTypeDescription
apiKeyStringRequired. Your environment API key.
apiVersionStringAPI version used by the client.
baseUrlStringOverride the API base URL.
timeoutDurationPer-request timeout. Default: 5 seconds.
defaultValuebooleanReturned on technical failures such as network or timeout errors.
strict4xxbooleanThrows ReleaseAnchorException on unexpected 4xx responses instead of silently falling back.
loggerBiConsumer<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(...) or reportFailure(...) automatically when evaluationId is 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 with EXECUTION_FAILED
  • throw → reports failure with UNKNOWN and 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();
Was this helpful?