A high-performance OpenFeature provider for Confidence feature flags that evaluates flags locally for minimal latency.
- Local Resolution: Evaluates feature flags locally using WebAssembly (WASM)
- Low Latency: No network calls during flag evaluation
- Automatic Sync: Periodically syncs flag configurations from Confidence
- Exposure Logging: Fully supported exposure logging (and other resolve analytics)
- Event Tracking: Send custom events via the OpenFeature
track()API - OpenFeature Compatible: Works with the standard OpenFeature SDK
- HTTP Proxy Service: Proxy requests from client SDKs through your backend for enhanced control
Add this dependency to your pom.xml:
<dependency>
<groupId>com.spotify.confidence</groupId>
<artifactId>openfeature-provider-local</artifactId>
<version>0.23.4</version>
</dependency>You'll need a client secret from Confidence to use this provider.
📖 See the Integration Guide: Getting Your Credentials for step-by-step instructions on:
- How to navigate the Confidence dashboard
- Creating a Backend integration
- Creating a test flag for verification
- Best practices for credential storage
The provider fetches encrypted flag state and decrypts it when loading the resolver. Pass the encryption key for your client credential when creating the provider.
See the Integration Guide for background and migration details.
Pass both credentials to the provider constructor:
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider("your-client-secret", "your-encryption-key");Each client credential has a unique encryption key, available alongside it in Confidence Admin.
import com.spotify.confidence.sdk.OpenFeatureLocalResolveProvider;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.MutableContext;
// Create and register the provider
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider("your-client-secret", "your-encryption-key"); // Get from Confidence dashboard
OpenFeatureAPI.getInstance().setProviderAndWait(provider); // Important: use setProviderAndWait()
// Get a client
Client client = OpenFeatureAPI.getInstance().getClient();
// Create evaluation context with user attributes for targeting
MutableContext ctx = new MutableContext("user-123");
ctx.add("country", "US");
ctx.add("plan", "premium");
// Evaluate a flag
Boolean enabled = client.getBooleanValue("test-flag.enabled", false, ctx);
System.out.println("Flag value: " + enabled);
// Don't forget to shutdown when your application exits (see Shutdown section)The evaluation context contains information about the user/session being evaluated for targeting and A/B testing.
// Simple attributes
MutableContext ctx = new MutableContext("user-123");
ctx.add("country", "US");
ctx.add("plan", "premium");
ctx.add("age", 25);The provider uses a default value fallback pattern - when evaluation fails, it returns your specified default value instead of throwing an error.
📖 See the Integration Guide: Error Handling for:
- Common failure scenarios
- Error codes and meanings
- Production best practices
- Monitoring recommendations
// The provider returns the default value on errors
Boolean enabled = client.getBooleanValue("my-flag.enabled", false, ctx);
// enabled will be 'false' if evaluation failed
// For detailed error information, use getBooleanDetails()
FlagEvaluationDetails<Boolean> details = client.getBooleanDetails("my-flag.enabled", false, ctx);
if (details.getErrorCode() != null) {
System.err.println("Flag evaluation error: " + details.getErrorMessage());
System.err.println("Reason: " + details.getReason());
}Call shutdown() when your application exits to flush exposure logs and clean up resources:
// Shutdown the provider to flush logs and clean up resources
OpenFeatureAPI.getInstance().shutdown();Note: OpenFeature Java SDK 1.20.2+ properly awaits provider shutdown (#1744). If you are using an older SDK version (< 1.20.2), call
OpenFeatureAPI.getInstance().getProvider().shutdown()directly instead to avoid losing exposure logs.
Pass the client secret and encryption key to the constructor. Use LocalProviderConfig.builder() for optional settings:
LocalProviderConfig config = LocalProviderConfig.builder()
.resolverPoolSize(4) // Number of WASM resolver instances (default: 2). Increase for higher concurrency.
.build();
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider(config, "your-client-secret", "your-encryption-key");Configure the provider behavior using environment variables:
CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS: How often to poll Confidence to get updates (default:30seconds)CONFIDENCE_MATERIALIZATION_READ_TIMEOUT_SECONDS: Timeout for materialization read operations when using remote materialization store (default:2seconds)CONFIDENCE_MATERIALIZATION_WRITE_TIMEOUT_SECONDS: Timeout for materialization write operations when using remote materialization store (default:5seconds)
The following environment variables are deprecated and will be removed in a future version. Use LocalProviderConfig.builder() instead:
CONFIDENCE_NUMBER_OF_WASM_INSTANCES: Use.resolverPoolSize()insteadCONFIDENCE_DOMAIN: Use a customChannelFactoryinsteadCONFIDENCE_GRPC_PLAINTEXT: Use a customChannelFactoryinstead
The built-in DefaultChannelFactory configures sensible gRPC connection defaults:
| Setting | Value | Purpose |
|---|---|---|
keepAliveTime |
5 min | Sends HTTP/2 pings to detect dead connections |
keepAliveTimeout |
20s | Time to wait for a keepalive ping response |
idleTimeout |
30 min | Closes channels with no active RPCs |
Retry on UNAVAILABLE |
3 attempts, 1s → 10s exponential backoff | Retries flag log writes on transient gRPC failures |
These defaults prevent connection-reset errors when intermediate load balancers (e.g., Envoy) cycle long-lived HTTP/2 connections. The retry policy handles transient failures (e.g., pod recycling, TLS resets) transparently via gRPC service config. If you provide a custom ChannelFactory, you may want to replicate these settings — see gRPC retry via service config.
For testing or advanced production scenarios, you can provide a custom ChannelFactory to control how gRPC channels are created:
import com.spotify.confidence.sdk.LocalProviderConfig;
import com.spotify.confidence.sdk.ChannelFactory;
// Example: Custom channel factory for testing with in-process server
ChannelFactory mockFactory = (target, interceptors) ->
InProcessChannelBuilder.forName("test-server")
.usePlaintext()
.intercept(interceptors.toArray(new ClientInterceptor[0]))
.build();
LocalProviderConfig config = new LocalProviderConfig(mockFactory);
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider(config, "client-secret", "your-encryption-key");This is particularly useful for:
- Unit testing: Inject in-process channels with mock gRPC servers
- Integration testing: Point to local test servers
- Production customization: Custom TLS settings, proxies, or connection pooling
- Debugging: Add custom logging or tracing interceptors
Apply-event deduplication is on by default. The WASM resolver collapses byte-identical apply events for the same unit and variant within a 120-second TTL window (tracking up to 100,000 entries), so repeatedly resolving the same flag for the same targeting key produces one apply event instead of many.
Upgrading without changing any code therefore reduces raw apply-event volume, substantially so for high-QPS traffic that resolves the same flag for the same unit. Distinct units are never collapsed, because the targeting key is part of the dedup key, so per-unit exposure counts are unaffected.
To turn it off and log every apply:
LocalProviderConfig config = LocalProviderConfig.builder()
.enableApplyDedup(false)
.build();The builder is the recommended way to opt out. If you construct
LocalProviderConfig directly, use the overload that takes enableApplyDedup
as its last argument:
LocalProviderConfig config =
new LocalProviderConfig(
channelFactory,
httpClientFactory,
/* useRemoteMaterializationStore= */ false,
LocalProviderConfig.DEFAULT_RESOLVER_POOL_SIZE,
/* enableApplyDedup= */ false);Every other constructor leaves dedup on.
The provider supports materializations for two key use cases:
-
Sticky Assignments: Maintain consistent variant assignments across evaluations even when targeting attributes change. This enables pausing intake (stopping new users from entering an experiment) while keeping existing users in their assigned variants. 📖 See the Integration Guide: Sticky Assignments for how sticky assignments work and their benefits.
-
Custom Targeting via Materialized Segments: Efficiently target precomputed sets of identifiers from datasets. Instead of evaluating complex targeting rules at runtime, materializations allow for fast lookups of whether a unit (user, session, etc.) is included in a target segment.
The provider offers three options for managing materialization data:
By default, materializations are not supported. If a flag requires materialization data (sticky assignments or custom targeting), the evaluation will fail and return the default value.
// Default behavior - no materialization support
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider("your-client-secret", "your-encryption-key");Enable remote materialization storage to have Confidence manage materialization data server-side. When sticky assignment data or materialized segment targeting data is needed, the provider makes a gRPC call to Confidence:
// Enable remote materialization storage
LocalProviderConfig config = LocalProviderConfig.builder()
.useRemoteMaterializationStore(true)
.build();
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider(config, "your-client-secret", "your-encryption-key");- Without materialization (default): Flag evaluation requires zero network calls - all evaluations happen locally with minimal latency
- With remote materialization: Flag evaluation requires network calls to Confidence for materialization reads/writes, adding latency to each evaluation
This option:
- Requires network calls for materialization reads/writes during flag evaluation
- Automatically handles TTL and cleanup
- Requires no additional infrastructure
- Suitable for production use cases where sticky assignments or materialized segment targeting are required
For advanced use cases requiring minimal latency, implement a custom MaterializationStore to manage materialization data in your own infrastructure (Redis, database, etc.):
// Custom storage for materialization data
MaterializationStore store = new RedisMaterializationStore(jedisPool);
OpenFeatureLocalResolveProvider provider = new OpenFeatureLocalResolveProvider(
new LocalProviderConfig(),
"your-client-secret",
"your-encryption-key",
store
);For detailed information on how to implement custom storage backends, see STICKY_RESOLVE.md.
See the InMemoryMaterializationStoreExample class in the test sources for a reference implementation, or review the MaterializationStore javadoc for detailed API documentation.
The FlagResolverService enables you to proxy flag resolution requests from client SDKs (like confidence-sdk-js) through your backend service. This is useful when you want:
- Low-latency resolution: Client SDKs make requests to your backend instead of Confidence servers
- Backend-controlled credentials: Client SDKs don't need their own client secrets
- Context enrichment: Add server-side context (user ID from auth, request metadata) before resolution
- JSON only: Only
application/jsoncontent type is supported. Requests with other content types will receive a 415 response.
⚠️ Resolve tokens returned withapply=falseare not encrypted. When a client SDK requests resolution withapply=false, the response includes a resolve token containing the full evaluation context and the resolved variant for each flag. If that data is sensitive, encrypt the token in your proxy before returning it to the client SDK and decrypt it again on the apply endpoint before forwarding toFlagResolverService. See the Integration Guide: Deferred Apply and Resolve Token Security.
import com.spotify.confidence.sdk.OpenFeatureLocalResolveProvider;
import com.spotify.confidence.sdk.FlagResolverService;
// Create and initialize the provider
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider("your-client-secret", "your-encryption-key");
// If you're not using OpenFeature, don't forget to call provider.initialize()
OpenFeatureAPI.getInstance().setProviderAndWait(provider);
// Create the HTTP service
FlagResolverService flagResolver = new FlagResolverService(provider);Add server-side context to requests before resolution:
FlagResolverService flagResolver = new FlagResolverService(provider,
ContextDecorator.sync((ctx, req) -> {
// Set targeting key from upstream auth middleware header
List<String> userIds = req.headers().get("X-User-Id");
if (userIds != null && !userIds.isEmpty()) {
return ctx.merge(new ImmutableContext(userIds.get(0)));
}
return ctx;
}));The service uses simple interfaces that can be adapted to any HTTP framework.
public class FlagServlet extends HttpServlet {
private final FlagResolverService flagResolverService;
public FlagServlet(OpenFeatureLocalResolveProvider provider) {
// Set targeting key from upstream auth middleware header
this.flagResolverService = new FlagResolverService(provider,
ContextDecorator.sync((context, request) -> {
List<String> userIds = request.headers().get("X-User-Id");
if (userIds != null && !userIds.isEmpty()) {
return context.merge(new ImmutableContext(userIds.get(0)));
}
return context;
}));
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
ConfidenceHttpResponse response;
if (req.getPathInfo().endsWith("v1/flags:resolve")) {
response = flagResolverService.handleResolve(toConfidenceRequest(req))
.toCompletableFuture().join();
} else if (req.getPathInfo().endsWith("v1/flags:apply")) {
response = flagResolverService.handleApply(toConfidenceRequest(req))
.toCompletableFuture().join();
} else {
resp.setStatus(404);
return;
}
resp.setStatus(response.statusCode());
response.headers().forEach(resp::setHeader);
resp.getOutputStream().write(response.body());
}
private ConfidenceHttpRequest toConfidenceRequest(HttpServletRequest req) {
// Cache body bytes since InputStream can only be read once
final byte[] bodyBytes;
try {
bodyBytes = req.getInputStream().readAllBytes();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return new ConfidenceHttpRequest() {
@Override
public String method() {
return req.getMethod();
}
@Override
public byte[] body() {
return bodyBytes;
}
@Override
public Map<String, List<String>> headers() {
Map<String, List<String>> headers = new HashMap<>();
Collections.list(req.getHeaderNames()).forEach(name ->
headers.put(name, Collections.list(req.getHeaders(name))));
return headers;
}
};
}
}Register the servlet at /confidence-flags/* to handle requests to /confidence-flags/v1/flags:resolve and /confidence-flags/v1/flags:apply.
Configure your client SDK (e.g., confidence-sdk-js) to use your backend:
import { Confidence } from '@spotify-confidence/sdk';
const confidence = Confidence.create({
clientSecret: 'not-used-but-required',
resolveBaseUrl: 'https://your-backend.com/confidence-flags',
applyBaseUrl: 'https://your-backend.com/confidence-flags',
});The provider supports the OpenFeature tracking API for sending custom events to the Confidence events backend. Event tracking is automatically enabled — no configuration needed.
📖 See the Integration Guide: Event Tracking for delivery guarantees, payload mapping rules, and cross-provider differences.
Client client = OpenFeatureAPI.getInstance().getClient();
// Track a simple event
MutableContext ctx = new MutableContext("user-123");
client.track("checkout_completed", ctx);
// Track with a numeric value
client.track("purchase", ctx, new MutableTrackingEventDetails(49.99f));
// Track with custom data
MutableTrackingEventDetails details = new MutableTrackingEventDetails(1.0f);
details.add("sku", "ABC-123");
details.add("category", "electronics");
client.track("item_added", ctx, details);Events are batched internally and flushed to the Confidence events service every 15 seconds. On shutdown, pending events are drained on a best-effort basis (up to 100 batches within a 5-second grace period).
By default, every flag evaluation records an exposure event (apply). Only disable this for exceptional cases where this provider must not collect exposures at all.
For normal feature delivery and experiments, keep applies enabled. When exposure collection is disabled, Confidence does not receive assignment/exposure events for those evaluations. Experiment results, exposure counts, assignment diagnostics, and downstream reporting that depend on exposures can be incomplete or unavailable. Resolve analytics and telemetry are still sent, so this is not a general logging or privacy-off switch.
To disable exposure collection for all OpenFeature evaluations through this provider, set disableExposureCollection on the provider config:
LocalProviderConfig config = LocalProviderConfig.builder().disableExposureCollection(true).build();
OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider(config, clientSecret, encryptionKey);To skip exposure collection for a single evaluation, pass _confidence_skip_apply in the evaluation context:
MutableContext context = new MutableContext("user-123");
context.add("_confidence_skip_apply", true);
boolean value = client.getBooleanValue("my-flag.enabled", false, context);The key is automatically stripped from the context before it reaches the resolver.
| Mechanism | Scope | Assignment/exposure events | Resolve logs and telemetry |
|---|---|---|---|
disableExposureCollection provider config |
All OpenFeature evaluations through this provider | Never queued; no deferred apply token is returned | Still sent |
_confidence_skip_apply context key |
One evaluation | No immediate exposure event for that evaluation | Still sent |
This is an advanced feature intended for exceptional cases. If you're considering using it, reach out to the Confidence team to discuss the best approach for your setup.
- Java 17+
- OpenFeature SDK 1.20.2+
See CONTRIBUTING.md for development setup and guidelines.
encryptionKey is now required when constructing the provider. Supply exactly 64
hexadecimal characters. Missing, empty, or malformed keys fail before network
activity. The provider only fetches encrypted state and never falls back to plaintext.
Open Confidence Admin → Clients, select your client, and locate the credential used by the provider. Each credential has its own unique encryption key, available alongside it. Use the key paired with your configured client secret. Configure and verify encryption on your existing SDK before upgrading.
All public provider constructors take the encryption key immediately after the
client secret, including new OpenFeatureLocalResolveProvider(clientSecret, encryptionKey)
and new OpenFeatureLocalResolveProvider(config, clientSecret, encryptionKey).
Move existing .encryptionKey(key) configuration into the provider constructor.
LocalProviderConfig contains optional settings only. Constructors accepting
injected state are package-private.