Skip to main content

Evaluation

Understanding how the Java SDK evaluates feature flags helps you design effective targeting rules.

Basic Evaluation​

The simplest form of evaluation checks if a feature is enabled:

boolean enabled = client.isEnabled("my-feature");

This returns true if the feature should be enabled based on:

  1. The feature definition from Toggly
  2. The current evaluation context
  3. Configured defaults

Evaluation Context​

Context provides information about the current user (and request) for targeting:

EvaluationContext context = EvaluationContext.builder()
.identity("user-123")
.addGroup("beta-testers")
.addGroup("premium-users")
.claim("role", "admin")
.trait("plan", "enterprise")
.request(RequestContext.builder()
.userAgent("Mozilla/5.0 ... Chrome/120.0.0.0 ...")
.acceptLanguage("en-US,en;q=0.9")
.country("US")
.build())
.build();

boolean enabled = client.isEnabled("enterprise-feature", context);
FieldUsed by
identityPercentage, Targeting, segment percentage gates
groupsTargeting group rules
traitsCustom filters you register
claimsUserClaims (Claim + Value exact match)
requestHTTP segment filters (browser / language / country / device / OS)
entity (per call)ContextProperty

UserClaims never reads HTTP headers — set claims from your authenticated principal / JWT.

Mapping HTTP headers​

HttpRequestMapper maps common headers into RequestContext (same precedence as Node fromHttpRequest). It does not invent identity, groups, or claims:

Request fieldHeader (first match wins)
userAgentuser-agent
acceptLanguageaccept-language
countrycf-ipcountry, then x-vercel-ip-country, then cloudfront-viewer-country

For a Jakarta Servlet or Spring MVC application, put this helper in src/main/java/example/RequestContexts.java. Authentication must run first. The admin role below is an application example: replace the role/claim mapping with your authenticated domain claims. Country headers must be overwritten by a trusted edge; browser/language inputs describe a request, not an authorization fact.

RequestContexts.java
package example;

import io.toggly.core.context.EvaluationContext;
import io.toggly.core.context.HttpRequestMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public final class RequestContexts {
private RequestContexts() {}

public static EvaluationContext from(HttpServletRequest request) {
Principal principal = request.getUserPrincipal();
EvaluationContext.Builder builder = EvaluationContext.builder();
if (principal != null) {
builder.identity(principal.getName());
builder.claim("role", request.isUserInRole("admin") ? "admin" : "member");
}
Map<String, String> headers = new HashMap<>();
if (request.getHeaderNames() != null) {
for (String name : Collections.list(request.getHeaderNames())) {
headers.put(name, request.getHeader(name));
}
}
return HttpRequestMapper.mergeInto(headers, builder.build());
}
}

Pass RequestContexts.from(request) explicitly to client.isEnabled, or install it as the resolver in the MVC configuration. Anonymous requests have no identity/claims here. If you need sticky anonymous rollouts, supply a stable session identity from your application before the first evaluation. Do not change the shared client's identity for each visitor.

The native Servlet context filter and Spring header resolvers populate identity and groups only. They do not map claims, request segments or entity data. For a Servlet application, put this extension beside the helper and use example.AuthenticatedTogglyContextFilter as the context filter class in web.xml:

AuthenticatedTogglyContextFilter.java
package example;

import io.toggly.core.context.EvaluationContext;
import io.toggly.servlet.TogglyContextFilter;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;

public class AuthenticatedTogglyContextFilter extends TogglyContextFilter {
@Override
protected EvaluationContext createContext(ServletRequest request) {
return request instanceof HttpServletRequest http
? RequestContexts.from(http) : EvaluationContext.empty();
}
}

The native filter scopes synchronous calls and clears context in finally.

Omit configured defaultIdentity in a multi-user application. If a context lacks identity while a default identity is configured, the SDK rebuilds it with only groups/traits, dropping claims/request/entity. Supplying each request's identity explicitly preserves those fields.

Entity context​

EvaluationContext identity / groups / claims / traits / request are user + request. Entity kinds are a separate object passed per evaluation. See Entity & page context.

Register a local mapper before creating the client. To add the kind to the dashboard catalog, pass an optional EntityContextRegistry.EntityContextSchemaRegistration as the third argument; registered schemas are sent at startup unless registerContextsOnStartup is disabled. Browser/mobile clients never register schemas.

import io.toggly.core.context.EntityContextRegistry;
import io.toggly.core.context.EvaluationContext;
import io.toggly.core.context.TogglyEntityContext;
import java.util.Map;

// Register once, before creating the client. Order is your domain type.
EntityContextRegistry.registerContext("Order", value -> {
Order mappedOrder = (Order) value;
return new TogglyEntityContext("Order", String.valueOf(mappedOrder.getId()),
Map.of("Status", mappedOrder.getStatus(), "Total", mappedOrder.getTotal()));
});

EvaluationContext orderContext = userContext.withEntity(
EntityContextRegistry.map("Order", order));
boolean on = client.isEnabled("OrderBadge", orderContext);

Keep thread-local user context user-only; derive the entity context for the individual evaluation. A missing entity fails closed for ContextProperty. The current evaluator checks the supplied properties but does not compare the entity kind against the definition's contextKind. The caller must supply the intended Order; an unrelated kind with matching properties is not rejected by kind.

Thread-Local Context​

Set context once for all evaluations in a thread:

// In a filter or interceptor
ContextHolder.setContext(context);

try {
// All evaluations use this context
client.isEnabled("feature-a");
client.isEnabled("feature-b");
} finally {
ContextHolder.clear();
}

ContextHolder is an ordinary ThreadLocal. It does not propagate through executors, Servlet async dispatch or Reactor subscriptions. Capture an immutable context and pass it to the async overload explicitly. For nested synchronous work, ContextHolder.runWithContext(context, runnable) restores the previous context in finally, including when the runnable throws.

Filter Evaluation​

Features are evaluated based on their configured filters.

Built-in filters​

EvaluatorRegistry pre-registers:

Filter namePurpose
AlwaysOn / AlwaysOffStatic on / off
PercentageDeterministic rollout from identity
TimeWindowTime-based availability
TargetingUsers / groups / default rollout %
BrowserFamily, BrowserLanguage, Country / CountryFamily, DeviceType, OS / OperatingSystemHTTP segment filters via request
UserClaimsPrincipal claims via claims
ContextPropertyEntity property filters (fail closed)

Missing request / claims fields, missing segment Percentage, or an unknown filter name fails closed. Per-SDK Local vs Worker: SDK × filter matrix.

DeviceType recognizes iPhone/iPad/iPod as Apple devices, but a Macintosh user-agent is classified as Other. Do not expect a Macintosh request to match an Apple device preset; OS targeting is a separate filter. Always test the actual user-agent strings your application receives.

AlwaysOn Filter​

Returns true for all contexts:

// Feature with AlwaysOn filter is always enabled
assertTrue(client.isEnabled("always-on-feature"));

Percentage Filter​

Deterministic percentage-based rollout:

// 50% rollout - consistent for same user
EvaluationContext user1 = EvaluationContext.builder()
.identity("user-1")
.build();

// Same user always gets same result
boolean result1 = client.isEnabled("gradual-rollout", user1);
boolean result2 = client.isEnabled("gradual-rollout", user1);
assertEquals(result1, result2); // Always equal

// Without identity, this 50% rollout returns false
assertFalse(client.isEnabled("gradual-rollout", EvaluationContext.empty()));

Targeting Filter​

Match specific users or groups:

// Feature targeted to specific users or groups
EvaluationContext betaUser = EvaluationContext.builder()
.identity("user-123")
.addGroup("beta-testers")
.build();

// Enabled if user or group is targeted
client.isEnabled("beta-feature", betaUser);

TimeWindow Filter​

Time-based availability:

// Feature available only during configured window
// Automatically enabled/disabled based on Start/End times
client.isEnabled("holiday-promo");

Requirement Types​

Features can require ALL or ANY filters to pass:

ANY (Default)​

Feature is enabled if any filter passes:

// Feature with 2 filters (Targeting + Percentage)
// Enabled if user is targeted OR falls in percentage

ALL​

Feature is enabled only if all filters pass:

// Feature with ALL requirement
// Enabled only if user passes every filter

Feature Gates​

Evaluate multiple features together:

// ALL features must be enabled
boolean canAccess = client.allEnabled(List.of(
"premium-plan",
"verified-email",
"beta-access"
));

// ANY feature must be enabled
boolean hasDiscount = client.anyEnabled(List.of(
"summer-sale",
"loyalty-discount",
"new-customer-offer"
));

// NONE of the features should be enabled
boolean showLegacy = client.noneEnabled(List.of(
"new-ui",
"beta-ui"
));

Gate with Options​

boolean result = client.gate(
List.of("feature-a", "feature-b"),
FeatureRequirement.ALL, // ALL or ANY
false, // negate
context
);

Evaluation with Defaults​

When features are not defined, defaults apply:

// Configure defaults
TogglyConfig config = TogglyConfig.builder()
.appKey("your-key")
.defaultFeatureState(false) // Global default
.featureDefault("my-feature", true) // Per-feature default
.build();

try (TogglyClient client = new TogglyClient(config)) {
// These assertions assume neither key has a downloaded definition.
assertFalse(client.isEnabled("unknown-feature")); // Global default.
assertTrue(client.isEnabled("my-feature")); // Per-feature default.
}

Conditional Execution​

Execute code based on feature state:

// Execute only if enabled
client.ifEnabled("analytics", () -> {
trackEvent("page_view");
});

// Execute one of two actions
client.ifEnabledElse("new-checkout",
() -> processNewCheckout(order),
() -> processLegacyCheckout(order)
);

// Get value based on feature
String template = client.getValue("new-ui", "template-v2", "template-v1");

// Lazy evaluation with suppliers
Config config = client.getValue("new-config",
() -> loadNewConfig(),
() -> loadLegacyConfig()
);

getValue chooses between the two values or suppliers you provide using a Boolean flag. It does not allocate experiment variants. Variant labels passed to usage/view telemetry are labels, not a native allocation API.

Bulk Evaluation​

Evaluate all features at once:

// Get all feature states
Map<String, Boolean> allFeatures = client.evaluateAll();

// With context
Map<String, Boolean> userFeatures = client.evaluateAll(context);

// Use the results
allFeatures.forEach((key, enabled) -> {
System.out.println(key + ": " + enabled);
});

Async Evaluation​

For asynchronous evaluation, pass the request context explicitly:

CompletableFuture<Boolean> future = client.isEnabledAsync("my-feature", context);

future.thenAccept(enabled -> {
if (enabled) {
// Feature is enabled
}
});

// With context
client.isEnabledAsync("my-feature", context)
.thenAccept(enabled -> processResult(enabled));

Evaluation Debugging​

Get feature definition for debugging:

// Get definition
FeatureDefinition definition = client.getFeatureDefinition("my-feature");

if (definition != null) {
System.out.println("Filters: " + definition.getFilters().size());
System.out.println("Requirement: " + definition.getRequirementType());
}

// List all feature keys
Set<String> keys = client.getFeatureKeys();