| name | openfeature-sdk-dev |
| description | Develop OpenFeature SDK implementations from the specification. Use when implementing the OpenFeature spec in a new language, extending existing SDKs with new features, building custom providers, or contributing to official SDK repositories. |
OpenFeature SDK Development
Note: This skill extends patterns from
meta-sdk-patterns-eng. See that skill for foundational SDK patterns (architecture, error handling, configuration, testing strategies, packaging).
Guide for implementing OpenFeature SDKs from the specification. This skill covers building SDK implementations, not using existing SDKs (see openfeature-eng for usage).
When to Use This Skill
- Implementing OpenFeature in a new programming language
- Contributing to official OpenFeature SDK repositories
- Building custom providers from scratch
- Extending existing SDKs with new features
- Understanding SDK architecture and compliance requirements
Specification Overview
OpenFeature uses RFC 2119 keywords (MUST, SHOULD, MAY) to define normative requirements. Implementations achieve compliance by satisfying all mandatory clauses.
Stability Levels
| Level | Description | Breaking Change Policy |
|---|---|---|
| Experimental | Testing features, may change anytime | None |
| Hardening | Production-ready, TSC consensus for changes | Requires consensus |
| Stable | Battle-tested, major version protection | Major version only |
Core Architecture
Component Hierarchy
┌─────────────────────────────────────────────────────────────┐
│ OpenFeature API │
│ (Global Singleton - manages providers, hooks, context) │
├─────────────────────────────────────────────────────────────┤
│ Client(s) │
│ (Domain-scoped flag evaluation interface) │
├─────────────────────────────────────────────────────────────┤
│ Provider(s) │
│ (Translates evaluation to flag management system) │
├─────────────────────────────────────────────────────────────┤
│ Hooks │
│ (Lifecycle middleware: before/after/error/finally) │
└─────────────────────────────────────────────────────────────┘
SDK Paradigms
Dynamic-Context (Server-Side)
- Context passed per evaluation call
- Stateless evaluation
- Multi-tenant support
Static-Context (Client-Side)
- Context set once, cached
- Requires reconciliation on context change
- Single-user/device focus
Type Definitions
Flag Value Types
// Core value types
type FlagValue = boolean | string | number | Structure;
// Structure: Language-idiomatic structured data (object, dict, map)
interface Structure {
[key: string]: boolean | string | number | DateTime | Structure;
}
Resolution Details
interface ResolutionDetails<T extends FlagValue> {
value: T; // Required: resolved flag value
variant?: string; // Optional: string identifier for value
reason?: ResolutionReason; // Optional: semantic cause
errorCode?: ErrorCode; // Optional: null/falsy on success
errorMessage?: string; // Optional: additional error detail
flagMetadata?: FlagMetadata; // Optional: arbitrary metadata
}
type ResolutionReason =
| 'STATIC' // Statically configured value
| 'DEFAULT' // Default value returned
| 'TARGETING_MATCH' // Targeting rule matched
| 'SPLIT' // Percentage/split allocation
| 'CACHED' // Cached value returned
| 'DISABLED' // Flag disabled
| 'UNKNOWN' // Reason unknown
| 'STALE' // Stale cached value
| 'ERROR' // Error occurred
| string; // Custom reasons allowed
Error Codes
enum ErrorCode {
PROVIDER_NOT_READY = 'PROVIDER_NOT_READY',
FLAG_NOT_FOUND = 'FLAG_NOT_FOUND',
PARSE_ERROR = 'PARSE_ERROR',
TYPE_MISMATCH = 'TYPE_MISMATCH',
TARGETING_KEY_MISSING = 'TARGETING_KEY_MISSING',
INVALID_CONTEXT = 'INVALID_CONTEXT',
PROVIDER_FATAL = 'PROVIDER_FATAL',
GENERAL = 'GENERAL'
}
Provider Status
enum ProviderStatus {
NOT_READY = 'NOT_READY', // Initial state, not yet initialized
READY = 'READY', // Initialization complete
ERROR = 'ERROR', // Temporary error, may recover
STALE = 'STALE', // Using cached/stale data
FATAL = 'FATAL', // Unrecoverable error
RECONCILING = 'RECONCILING' // Static-context only: context change in progress
}
API Implementation
Global Singleton (Requirement 1.1.1)
The API SHOULD exist as a global singleton, even with multiple API versions present.
// TypeScript example structure
class OpenFeatureAPI {
private static instance: OpenFeatureAPI;
private providers: Map<string, FeatureProvider> = new Map();
private hooks: Hook[] = [];
private globalContext: EvaluationContext = {};
static getInstance(): OpenFeatureAPI {
if (!OpenFeatureAPI.instance) {
OpenFeatureAPI.instance = new OpenFeatureAPI();
}
return OpenFeatureAPI.instance;
}
}
Provider Management
interface OpenFeatureAPI {
// Requirement 1.1.2: Set default provider
setProvider(provider: FeatureProvider): void;
// Requirement 1.1.2: Set provider and await initialization
setProviderAndWait(provider: FeatureProvider): Promise<void>;
// Requirement 1.1.3: Bind provider to domain
setProvider(domain: string, provider: FeatureProvider): void;
// Requirement 1.1.5: Get provider metadata
getProviderMetadata(domain?: string): ProviderMetadata;
// Requirement 1.6.1: Shutdown all providers
shutdown(): Promise<void>;
}
Provider Lifecycle Rules:
- MUST invoke
initializeon newly registered provider before resolution - MUST call
shutdownon previously registered provider when replaced - Requirement 1.6.2: Shutdown MUST reset all API state
Hook Management
interface OpenFeatureAPI {
// Requirement 1.1.4: Add hooks (append to collection)
addHooks(...hooks: Hook[]): void;
// Clear hooks
clearHooks(): void;
}
Client Creation
interface OpenFeatureAPI {
// Requirement 1.1.6: Create client with optional domain
getClient(domain?: string): Client;
}
Requirement 1.1.7: Client creation MUST NOT throw or abnormally terminate.
Client Implementation
Client Interface
interface Client {
// Requirement 1.2.2: Immutable domain field
readonly domain?: string;
// Client-level hooks
addHooks(...hooks: Hook[]): void;
// Client-level context (dynamic paradigm)
setContext(context: EvaluationContext): void;
}
Flag Evaluation Methods
Dynamic-Context Paradigm (Requirement 1.3.1.1)
interface Client {
getBooleanValue(
flagKey: string,
defaultValue: boolean,
context?: EvaluationContext,
options?: EvaluationOptions
): boolean;
getStringValue(
flagKey: string,
defaultValue: string,
context?: EvaluationContext,
options?: EvaluationOptions
): string;
getNumberValue(
flagKey: string,
defaultValue: number,
context?: EvaluationContext,
options?: EvaluationOptions
): number;
getObjectValue<T extends Structure>(
flagKey: string,
defaultValue: T,
context?: EvaluationContext,
options?: EvaluationOptions
): T;
}
Static-Context Paradigm (Requirement 1.3.2.1)
Context parameter omitted; set at API/client level.
interface StaticContextClient {
getBooleanValue(
flagKey: string,
defaultValue: boolean,
options?: EvaluationOptions
): boolean;
// ... same pattern for other types
}
Requirement 1.3.3.1: Languages with separate integer/float types SHOULD provide distinct methods.
Detailed Evaluation Methods
Return EvaluationDetails with supplementary metadata.
interface EvaluationDetails<T extends FlagValue> extends ResolutionDetails<T> {
flagKey: string; // Requirement 1.4.5: Must contain passed flag key
}
interface Client {
getBooleanDetails(
flagKey: string,
defaultValue: boolean,
context?: EvaluationContext,
options?: EvaluationOptions
): EvaluationDetails<boolean>;
// ... same pattern for string, number, object
}
Type Safety (Requirement 1.3.4)
If provider returns wrong type, return default value:
function evaluateFlag<T>(flagKey: string, defaultValue: T): T {
const result = provider.resolve(flagKey, defaultValue);
// Type mismatch → return default
if (typeof result.value !== typeof defaultValue) {
return defaultValue;
}
return result.value as T;
}
Error Handling (Requirement 1.4.10)
CRITICAL: Methods MUST NOT throw exceptions. Always return default value on error.
function getBooleanValue(flagKey: string, defaultValue: boolean): boolean {
try {
const details = provider.resolveBooleanValue(flagKey, defaultValue, context);
return details.value;
} catch (error) {
// Log via hooks, not direct logging (Requirement 1.4.11)
return defaultValue;
}
}
Evaluation Options
interface EvaluationOptions {
// Requirement 1.5.1: Additional hooks for this evaluation
hooks?: Hook[];
// Hook hints passed to all hooks
hookHints?: HookHints;
}
Provider Implementation
Provider Interface
interface FeatureProvider {
// Requirement 2.1.1: Metadata with name field
readonly metadata: ProviderMetadata;
// Requirement 2.4.1: Optional initialization
initialize?(context: EvaluationContext): Promise<void>;
// Requirement 2.5.1: Optional shutdown
shutdown?(): Promise<void>;
// Requirement 2.6.1: Optional context change handler (static paradigm)
onContextChange?(
oldContext: EvaluationContext,
newContext: EvaluationContext
): Promise<void>;
// Requirement 2.3.1: Optional provider hooks
hooks?: Hook[];
// Requirement 2.2.1: Resolution methods
resolveBooleanValue(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext
): ResolutionDetails<boolean>;
resolveStringValue(
flagKey: string,
defaultValue: string,
context: EvaluationContext
): ResolutionDetails<string>;
resolveNumberValue(
flagKey: string,
defaultValue: number,
context: EvaluationContext
): ResolutionDetails<number>;
resolveObjectValue<T extends Structure>(
flagKey: string,
defaultValue: T,
context: EvaluationContext
): ResolutionDetails<T>;
}
interface ProviderMetadata {
name: string;
}
Provider Lifecycle States
┌──────────────────┐
│ NOT_READY │
│ (Initial State) │
└────────┬─────────┘
│ initialize()
┌────────┴─────────┐
success │ │ failure
┌───────────┤ ├───────────┐
▼ │ │ ▼
┌───────────┐ │ │ ┌───────────┐
│ READY │◄──────┘ └────│ ERROR │
└─────┬─────┘ └─────┬─────┘
│ │
│ config change / recovery │ recovery
│ ┌─────────┐ │
├─┤ STALE │◄──────────────────────────────┘
│ └─────────┘
│
│ shutdown()
▼
┌───────────┐
│ NOT_READY │
└───────────┘
┌───────────┐
│ FATAL │ (Unrecoverable - no transitions out)
└───────────┘
Resolution Details Requirements
function resolveValue<T>(flagKey: string, defaultValue: T): ResolutionDetails<T> {
// Requirement 2.2.3: Value field MUST contain resolved value
// Requirement 2.2.4: Variant SHOULD be set if available
// Requirement 2.2.5: Reason SHOULD indicate semantic cause
// Requirement 2.2.6: Error code MUST be null/falsy on success
// Requirement 2.2.9: Flag metadata SHOULD be populated
return {
value: resolvedValue,
variant: 'variant-a',
reason: 'TARGETING_MATCH',
errorCode: undefined,
flagMetadata: { version: '1.0.0' }
};
}
Error Handling in Providers
function resolveValue(flagKey: string): ResolutionDetails {
try {
// Normal resolution
return { value: resolved, reason: 'TARGETING_MATCH' };
} catch (error) {
// Requirement 2.2.7: Indicate errors with error codes
// Requirement 2.3.3: May include error message
return {
value: defaultValue,
reason: 'ERROR',
errorCode: 'GENERAL',
errorMessage: error.message
};
}
}
Provider Shutdown (Requirement 2.5.2-2.5.3)
class MyProvider implements FeatureProvider {
private initialized = false;
async shutdown(): Promise<void> {
if (!this.initialized) return; // Idempotent
// Clean up resources
await this.connection?.close();
this.initialized = false;
// Requirement 2.5.2: Revert to uninitialized state
}
}
Evaluation Context
Context Structure
interface EvaluationContext {
// Requirement 3.1.1: Optional targeting key
targetingKey?: string;
// Requirement 3.1.2: Custom fields
[key: string]: boolean | string | number | DateTime | Structure | undefined;
}
Context Merging (Requirement 3.2.3)
Precedence order (highest wins):
- Before hooks (highest)
- Invocation context
- Client context
- Transaction context
- API (global) context (lowest)
function mergeContext(...contexts: EvaluationContext[]): EvaluationContext {
// Later contexts override earlier ones
return contexts.reduce((merged, ctx) => ({
...merged,
...ctx
}), {});
}
// Usage in evaluation
const finalContext = mergeContext(
api.getContext(), // Lowest precedence
transactionContext,
client.getContext(),
invocationContext,
beforeHookContext // Highest precedence
);
Hooks Implementation
Hook Interface
interface Hook {
// At least one stage required
before?(context: HookContext, hints: HookHints): EvaluationContext | void;
after?(context: HookContext, details: EvaluationDetails, hints: HookHints): void;
error?(context: HookContext, error: Error, hints: HookHints): void;
finally?(context: HookContext, hints: HookHints): void;
}
interface HookContext {
readonly flagKey: string;
readonly flagValueType: FlagValueType;
readonly defaultValue: FlagValue;
readonly evaluationContext: EvaluationContext;
readonly clientMetadata: ClientMetadata;
readonly providerMetadata: ProviderMetadata;
// Mutable: allows inter-stage communication
hookData: Record<string, unknown>;
}
interface HookHints {
readonly [key: string]: boolean | string | number | DateTime | Structure;
}
Hook Execution Order
API Hooks ──┬── before() ────────────────────────────────────┐
│ │
Client Hooks┼── before() ───────────────────────────────────┐│
│ ││
Invocation ─┼── before() ──────────────────────────────────┐││
Hooks │ │││
│ │││
Provider ───┼── before() ─────────────────────────────────┐││││
Hooks │ │││││
│ ┌─────────────────┐ │││││
│ │ FLAG RESOLVE │ │││││
│ └─────────────────┘ │││││
│ │││││
Provider ───┼── after() ──────────────────────────────────┘││││
Hooks │ ││││
│ ││││
Invocation ─┼── after() ───────────────────────────────────┘│││
Hooks │ │││
│ │││
Client Hooks┼── after() ────────────────────────────────────┘││
│ ││
API Hooks ──┴── after() ─────────────────────────────────────┘│
│
finally() executes in same reverse order ◄─────────┘
Hook Error Handling
async function executeHooks(hooks: Hook[], stage: string): Promise<void> {
for (const hook of hooks) {
try {
await hook[stage]?.();
} catch (error) {
// Hooks MUST NOT propagate exceptions
// Error hooks still execute
// Finally hooks always execute
if (stage !== 'error' && stage !== 'finally') {
await executeErrorHooks(hooks, error);
}
}
}
}
Before Hook Context Modification
// Dynamic-context paradigm only
function beforeHook(context: HookContext): EvaluationContext {
// Can return modified context (merged with highest precedence)
return {
...context.evaluationContext,
additionalKey: 'added-by-hook'
};
}
Events Implementation
Event Types
enum ProviderEvent {
PROVIDER_READY = 'PROVIDER_READY',
PROVIDER_ERROR = 'PROVIDER_ERROR',
PROVIDER_FATAL = 'PROVIDER_FATAL',
PROVIDER_STALE = 'PROVIDER_STALE',
PROVIDER_CONFIGURATION_CHANGED = 'PROVIDER_CONFIGURATION_CHANGED',
PROVIDER_RECONCILING = 'PROVIDER_RECONCILING', // Static-context only
PROVIDER_CONTEXT_CHANGED = 'PROVIDER_CONTEXT_CHANGED' // Static-context only
}
interface EventDetails {
providerName: string;
errorCode?: ErrorCode;
errorMessage?: string;
flagsChanged?: string[];
metadata?: Record<string, unknown>;
}
type EventHandler = (details: EventDetails) => void;
Event Handler Registration
interface EventEmitter {
// Requirement 5.2.1: API-level handlers
addHandler(event: ProviderEvent, handler: EventHandler): void;
// Requirement 5.2.2: Client-level handlers
addHandler(event: ProviderEvent, handler: EventHandler): void;
// Requirement 5.2.7: Handler removal
removeHandler(event: ProviderEvent, handler: EventHandler): void;
}
Event Emission Rules
class ProviderWrapper {
private handlers: Map<ProviderEvent, EventHandler[]> = new Map();
async initialize(): Promise<void> {
try {
await this.provider.initialize?.(this.context);
// Requirement 5.3.1: Emit READY on success
this.emit(ProviderEvent.PROVIDER_READY);
} catch (error) {
// Requirement 5.3.2: Emit ERROR on failure
this.emit(ProviderEvent.PROVIDER_ERROR, {
errorCode: 'GENERAL',
errorMessage: error.message
});
}
}
private emit(event: ProviderEvent, details?: Partial<EventDetails>): void {
const handlers = this.handlers.get(event) ?? [];
for (const handler of handlers) {
try {
handler({ providerName: this.provider.metadata.name, ...details });
} catch {
// Requirement 5.2.5: Handler errors don't prevent other handlers
}
}
}
}
Deferred Handler Execution (Requirement 5.3.3)
function addHandler(event: ProviderEvent, handler: EventHandler): void {
this.handlers.get(event)?.push(handler);
// If provider already in target state, execute immediately
if (event === ProviderEvent.PROVIDER_READY && this.status === 'READY') {
handler({ providerName: this.provider.metadata.name });
}
}
Provider Status Management
Status Tracking (Requirement 1.7.1)
class ProviderManager {
private status: ProviderStatus = ProviderStatus.NOT_READY;
getStatus(): ProviderStatus {
return this.status;
}
async initialize(): Promise<void> {
try {
await this.provider.initialize?.(this.context);
// Requirement 1.7.3: READY on normal initialization
this.status = ProviderStatus.READY;
} catch (error) {
if (error.code === 'PROVIDER_FATAL') {
// Requirement 1.7.5: FATAL on fatal error
this.status = ProviderStatus.FATAL;
} else {
// Requirement 1.7.4: ERROR on abnormal termination
this.status = ProviderStatus.ERROR;
}
}
}
}
Evaluation During Non-Ready States
function evaluate(flagKey: string, defaultValue: boolean): EvaluationDetails<boolean> {
// Requirement 1.7.6: Return error code if NOT_READY
if (this.status === ProviderStatus.NOT_READY) {
return {
value: defaultValue,
flagKey,
reason: 'ERROR',
errorCode: ErrorCode.PROVIDER_NOT_READY
};
}
// Requirement 1.7.7: Return error code if FATAL
if (this.status === ProviderStatus.FATAL) {
return {
value: defaultValue,
flagKey,
reason: 'ERROR',
errorCode: ErrorCode.PROVIDER_FATAL
};
}
// Normal evaluation
return this.provider.resolveBooleanValue(flagKey, defaultValue, this.context);
}
Testing Requirements
Gherkin Test Suites
The specification includes Gherkin test suites for compliance testing:
Feature: Flag Evaluation
Scenario: Boolean flag evaluation returns expected value
Given a provider is registered
When a boolean flag "my-flag" is evaluated with default "false"
Then the returned value should be "true"
And the variant should be "on"
And the reason should be "TARGETING_MATCH"
Scenario: Evaluation returns default on provider error
Given a provider is registered that throws errors
When a boolean flag "error-flag" is evaluated with default "false"
Then the returned value should be "false"
And the error code should be "GENERAL"
Compliance Testing Structure
// Test categories to implement
describe('Flag Evaluation API', () => {
describe('Singleton behavior', () => {
it('should return same instance', () => {
expect(OpenFeature.getInstance()).toBe(OpenFeature.getInstance());
});
});
describe('Provider management', () => {
it('should call initialize on provider registration');
it('should call shutdown on previous provider when replaced');
});
describe('Flag evaluation', () => {
it('should return correct type for boolean flags');
it('should return default value on type mismatch');
it('should never throw exceptions');
});
describe('Hooks', () => {
it('should execute in correct order');
it('should not propagate hook errors');
});
describe('Events', () => {
it('should emit PROVIDER_READY on successful initialization');
it('should execute deferred handlers immediately');
});
});
SDK Implementation Checklist
Phase 1: Core Types
- Define FlagValue union type
- Define ResolutionDetails structure
- Define EvaluationDetails structure
- Implement ErrorCode enum
- Implement ProviderStatus enum
- Implement ResolutionReason type
- Define EvaluationContext interface
- Define FlagMetadata type
Phase 2: Provider Interface
- Define FeatureProvider interface
- Define ProviderMetadata interface
- Implement NoOpProvider (for testing/defaults)
- Support initialize lifecycle method
- Support shutdown lifecycle method
- Support onContextChange (static paradigm)
Phase 3: API Implementation
- Implement global singleton
- Implement provider registration
- Implement domain-scoped providers
- Implement hook registration
- Implement context management
- Implement shutdown
Phase 4: Client Implementation
- Implement client creation (non-throwing)
- Implement value methods (boolean, string, number, object)
- Implement details methods
- Implement client-level hooks
- Implement client-level context
- Ensure type safety
Phase 5: Hooks
- Define Hook interface
- Define HookContext interface
- Define HookHints interface
- Implement hook execution order
- Implement error isolation
- Implement finally stage guarantee
- Support context modification (dynamic)
Phase 6: Events
- Define ProviderEvent enum
- Define EventDetails interface
- Implement event emitter
- Implement handler registration
- Implement deferred execution
- Ensure handler error isolation
Phase 7: Testing
- Unit tests for all components
- Integration tests with providers
- Gherkin compliance tests
- Edge case coverage
- Concurrency testing (if applicable)
Server SDK Implementation
Server SDKs use the dynamic-context paradigm where evaluation context is passed per request. This is the most common pattern for backend services.
Key Characteristics
| Aspect | Server SDK Behavior |
|---|---|
| Context | Passed per evaluation call |
| State | Stateless between evaluations |
| Concurrency | Multi-threaded, concurrent evaluations |
| Users | Multi-tenant (many users per instance) |
| Lifecycle | Long-running process |
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Server Application │
├─────────────────────────────────────────────────────────────┤
│ Request 1 Request 2 Request 3 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Context A│ │Context B│ │Context C│ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ OpenFeature API │ (Singleton) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Provider │ (Shared, thread-safe) │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Server SDK Requirements
// Dynamic-context evaluation signature
interface ServerClient {
getBooleanValue(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext, // Required per-call
options?: EvaluationOptions
): boolean;
}
// Context passed with each request
app.get('/api/feature', (req, res) => {
const context: EvaluationContext = {
targetingKey: req.user.id,
email: req.user.email,
plan: req.user.subscription,
// Request-specific context
};
const enabled = client.getBooleanValue('new-feature', false, context);
res.json({ enabled });
});
Server-Specific Considerations
Thread Safety
// Provider must be thread-safe
class ThreadSafeProvider implements FeatureProvider {
private cache: Map<string, FlagValue> = new Map();
private mutex = new Mutex();
async resolveBooleanValue(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext
): Promise<ResolutionDetails<boolean>> {
// Use mutex for cache access in multi-threaded environments
return this.mutex.runExclusive(async () => {
// Resolution logic
});
}
}
Connection Pooling
// Go - Reuse HTTP connections
type Provider struct {
client *http.Client
}
func NewProvider() *Provider {
return &Provider{
client: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
},
}
}
Request-Scoped Context Propagation
# Python - Using contextvars for request context
from contextvars import ContextVar
request_context: ContextVar[EvaluationContext] = ContextVar('request_context')
@app.middleware("http")
async def add_context(request: Request, call_next):
context = EvaluationContext(
targeting_key=request.user.id,
attributes={"path": request.url.path}
)
token = request_context.set(context)
try:
response = await call_next(request)
finally:
request_context.reset(token)
return response
Server SDK Checklist
- Thread-safe provider implementation
- Connection pooling for external calls
- Request context propagation
- Graceful shutdown with in-flight request handling
- Metrics/telemetry per evaluation
- Bulk evaluation support (optional)
- Caching strategy for high-throughput
Client SDK Implementation
Client SDKs use the static-context paradigm where context is set once and cached. This pattern is used for mobile apps, browser applications, and edge devices.
Key Characteristics
| Aspect | Client SDK Behavior |
|---|---|
| Context | Set once, cached until changed |
| State | Stateful, maintains flag cache |
| Concurrency | Single-threaded (usually) |
| Users | Single user/device per instance |
| Lifecycle | Application lifecycle (may be short-lived) |
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Client Application │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ EvaluationContext │ │
│ │ (Set once: user ID, device, app version, etc.) │ │
│ └──────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ OpenFeature API │ │
│ │ ┌────────────────────────────────────────────────┐ │ │
│ │ │ Flag Cache │ │ │
│ │ │ (Evaluated flags stored locally) │ │ │
│ │ └────────────────────────────────────────────────┘ │ │
│ └──────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Provider │ │
│ │ (Syncs with server, manages local cache) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Client SDK Requirements
// Static-context evaluation signature (no context parameter)
interface ClientSdkClient {
getBooleanValue(
flagKey: string,
defaultValue: boolean,
options?: EvaluationOptions
): boolean; // Synchronous - uses cached values
}
// Context set at initialization or on user change
OpenFeature.setContext({
targetingKey: user.id,
email: user.email,
deviceType: 'mobile',
appVersion: '2.1.0',
});
// Evaluations use cached context
const enabled = client.getBooleanValue('new-feature', false);
Client-Specific Events
// Additional events for client SDKs
enum ClientProviderEvent {
// Standard events
PROVIDER_READY = 'PROVIDER_READY',
PROVIDER_ERROR = 'PROVIDER_ERROR',
PROVIDER_STALE = 'PROVIDER_STALE',
// Client-specific events
PROVIDER_RECONCILING = 'PROVIDER_RECONCILING', // Context change in progress
PROVIDER_CONTEXT_CHANGED = 'PROVIDER_CONTEXT_CHANGED', // Context change complete
}
// Listen for context reconciliation
client.addHandler(ProviderEvent.PROVIDER_CONTEXT_CHANGED, () => {
// Re-render UI with new flag values
refreshUI();
});
Context Reconciliation
When context changes, client SDKs must reconcile cached values:
interface ClientProvider extends FeatureProvider {
// Called when context changes
onContextChange(
oldContext: EvaluationContext,
newContext: EvaluationContext
): Promise<void>;
}
class MyClientProvider implements ClientProvider {
private cache: Map<string, ResolutionDetails<FlagValue>> = new Map();
async onContextChange(
oldContext: EvaluationContext,
newContext: EvaluationContext
): Promise<void> {
// Emit RECONCILING event
this.emit(ProviderEvent.PROVIDER_RECONCILING);
try {
// Fetch new flag values for new context
const newFlags = await this.fetchFlags(newContext);
this.cache = new Map(newFlags);
// Emit CONTEXT_CHANGED event
this.emit(ProviderEvent.PROVIDER_CONTEXT_CHANGED);
} catch (error) {
this.emit(ProviderEvent.PROVIDER_ERROR, { error });
}
}
}
Platform-Specific Patterns
Mobile (iOS/Android)
// Swift - iOS lifecycle handling
class OpenFeatureManager {
func applicationDidBecomeActive() {
// Refresh flags when app becomes active
provider.refresh()
}
func applicationDidEnterBackground() {
// Persist cache before backgrounding
provider.persistCache()
}
}
// Kotlin - Android lifecycle
class FeatureFlagViewModel : ViewModel() {
private val client = OpenFeature.getClient()
init {
// Observe provider events
OpenFeature.addHandler(ProviderEvent.PROVIDER_READY) {
refreshFlags()
}
}
override fun onCleared() {
// Cleanup when ViewModel is destroyed
OpenFeature.shutdown()
}
}
Browser/Web
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// Refresh flags when tab becomes visible
provider.refresh();
}
});
// Handle online/offline
window.addEventListener('online', () => {
provider.reconnect();
});
window.addEventListener('offline', () => {
// Use cached values, emit STALE if needed
provider.setStatus(ProviderStatus.STALE);
});
Edge/Embedded
// Rust - Resource-constrained environments
pub struct EdgeProvider {
cache: HashMap<String, FlagValue>,
max_cache_size: usize,
storage: Box<dyn PersistentStorage>,
}
impl EdgeProvider {
pub fn new(max_cache_size: usize) -> Self {
Self {
cache: HashMap::with_capacity(max_cache_size),
max_cache_size,
storage: Box::new(FileStorage::new("/data/flags")),
}
}
// Load from persistent storage on init
pub async fn initialize(&mut self) -> Result<(), Error> {
self.cache = self.storage.load().await?;
Ok(())
}
}
Offline Support
interface OfflineCapableProvider extends ClientProvider {
// Check if operating offline
isOffline(): boolean;
// Get cached value (works offline)
getCachedValue<T>(flagKey: string): T | undefined;
// Queue context changes for when online
queueContextChange(context: EvaluationContext): void;
}
class OfflineProvider implements OfflineCapableProvider {
private pendingContext: EvaluationContext | null = null;
async onContextChange(old: EvaluationContext, new_: EvaluationContext) {
if (this.isOffline()) {
this.pendingContext = new_;
return; // Will reconcile when back online
}
await this.reconcile(new_);
}
async onOnline() {
if (this.pendingContext) {
await this.reconcile(this.pendingContext);
this.pendingContext = null;
}
}
}
Client SDK Checklist
- Static-context evaluation methods (no context param)
- Context reconciliation (
onContextChange) - RECONCILING and CONTEXT_CHANGED events
- Local flag cache
- Persistent storage for offline support
- Application lifecycle handling
- Network state awareness (online/offline)
- Background refresh strategies
- Memory-efficient caching (size limits)
- Synchronous evaluation from cache
Language-Specific Considerations
Static vs Dynamic Typing
Statically Typed (Go, Rust, Java, TypeScript)
- Use generics for type-safe evaluation
- Compile-time type checking preferred
- Requirement 2.2.8.1: Support generic resolution details
Dynamically Typed (Python, Ruby, JavaScript)
- Runtime type checking in evaluation
- Duck typing for providers
- Clear documentation of expected types
Concurrency Models
Single-threaded (JavaScript/Browser)
- Async/await for provider operations
- Event loop considerations
- Requirement 1.4.12: Provide async mechanisms
Multi-threaded (Go, Rust, Java)
- Thread-safe singleton implementation
- Concurrent provider access
- Lock-free where possible
Error Handling Idioms
Exceptions (Java, Python, C#)
- Catch and suppress in evaluation
- Return defaults on any exception
Result Types (Rust, Go)
- Map errors to default values
- Error details in resolution structure
Memory Management
Garbage Collected (Java, Go, Python)
- Standard cleanup in shutdown
Manual/RAII (Rust, C++)
- Implement Drop/destructor
- Consider Rc/Arc for shared providers