Confidence OpenFeature Local Provider for Java

June 30, 2026 ยท View on GitHub

Status: Experimental

A high-performance OpenFeature provider for Confidence feature flags that evaluates flags locally for minimal latency.

Features

  • 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)
  • OpenFeature Compatible: Works with the standard OpenFeature SDK
  • HTTP Proxy Service: Proxy requests from client SDKs through your backend for enhanced control

Installation

Add this dependency to your pom.xml:

<dependency>
    <groupId>com.spotify.confidence</groupId>
    <artifactId>openfeature-provider-local</artifactId>
    <version>0.15.2</version>
</dependency>

Getting Your Credentials

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

Quick Start

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"); // 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)

Evaluation Context

The evaluation context contains information about the user/session being evaluated for targeting and A/B testing.

Java-Specific Examples

// Simple attributes
MutableContext ctx = new MutableContext("user-123");
ctx.add("country", "US");
ctx.add("plan", "premium");
ctx.add("age", 25);

Error Handling

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

Java-Specific Examples

// 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());
}

Shutdown

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.

Configuration

Provider Configuration

Use LocalProviderConfig.builder() to configure the provider:

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");

Environment Variables

Configure the provider behavior using environment variables:

  • CONFIDENCE_RESOLVER_POLL_INTERVAL_SECONDS: How often to poll Confidence to get updates (default: 30 seconds)
  • CONFIDENCE_MATERIALIZATION_READ_TIMEOUT_SECONDS: Timeout for materialization read operations when using remote materialization store (default: 2 seconds)
  • CONFIDENCE_MATERIALIZATION_WRITE_TIMEOUT_SECONDS: Timeout for materialization write operations when using remote materialization store (default: 5 seconds)

Deprecated Environment Variables

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() instead
  • CONFIDENCE_DOMAIN: Use a custom ChannelFactory instead
  • CONFIDENCE_GRPC_PLAINTEXT: Use a custom ChannelFactory instead

Custom Channel Factory (Advanced)

The built-in DefaultChannelFactory configures sensible gRPC connection defaults:

SettingValuePurpose
keepAliveTime5 minSends HTTP/2 pings to detect dead connections
keepAliveTimeout20sTime to wait for a keepalive ping response
idleTimeout30 minCloses channels with no active RPCs
Retry on UNAVAILABLE3 attempts, 1s โ†’ 10s exponential backoffRetries 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");

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

Materializations

The provider supports materializations for two key use cases:

  1. 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.

  2. 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.

Materialization Storage Options

The provider offers three options for managing materialization data:

1. No Materialization Support (Default)

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");

2. Remote Materialization Store

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");

โš ๏ธ Important Performance Impact: This option fundamentally changes how the provider operates:

  • 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

3. Custom Materialization Storage

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",
    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.

HTTP Proxy Service (FlagResolverService)

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/json content type is supported. Requests with other content types will receive a 415 response.

โš ๏ธ Resolve tokens returned with apply=false are not encrypted. When a client SDK requests resolution with apply=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 to FlagResolverService. See the Integration Guide: Deferred Apply and Resolve Token Security.

Basic Setup

import com.spotify.confidence.sdk.OpenFeatureLocalResolveProvider;
import com.spotify.confidence.sdk.FlagResolverService;

// Create and initialize the provider
OpenFeatureLocalResolveProvider provider =
    new OpenFeatureLocalResolveProvider("your-client-secret");
// 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);

With Context Decoration

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;
    }));

Framework Integration

The service uses simple interfaces that can be adapted to any HTTP framework.

Servlet Example

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.

Client SDK Configuration

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',
});

Advanced: Controlling Exposure Events

By default, every flag evaluation triggers an exposure event (apply). If you need to resolve a flag without recording an exposure, you can 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.

This is an advanced feature intended for specific use cases such as prefetching or background evaluation. If you're considering using it, reach out to the Confidence team to discuss the best approach for your setup.

Requirements

  • Java 17+
  • OpenFeature SDK 1.20.2+

Contributing

See CONTRIBUTING.md for development setup and guidelines.