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:
- The feature definition from Toggly
- The current evaluation context
- 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);
| Field | Used by |
|---|---|
identity | Percentage, Targeting, segment percentage gates |
groups | Targeting group rules |
traits | Custom filters you register |
claims | UserClaims (Claim + Value exact match) |
request | HTTP 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 field | Header (first match wins) |
|---|---|
userAgent | user-agent |
acceptLanguage | accept-language |
country | cf-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.
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:
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 name | Purpose |
|---|---|
AlwaysOn / AlwaysOff | Static on / off |
Percentage | Deterministic rollout from identity |
TimeWindow | Time-based availability |
Targeting | Users / groups / default rollout % |
BrowserFamily, BrowserLanguage, Country / CountryFamily, DeviceType, OS / OperatingSystem | HTTP segment filters via request |
UserClaims | Principal claims via claims |
ContextProperty | Entity 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();