Agents openfeature-sdk-dev
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.
git clone https://github.com/aRustyDev/agents
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/openfeature-sdk-dev" ~/.claude/skills/arustydev-agents-openfeature-sdk-dev && rm -rf "$T"
content/skills/openfeature-sdk-dev/SKILL.mdOpenFeature SDK Development
Note: This skill extends patterns from
. See that skill for foundational SDK patterns (architecture, error handling, configuration, testing strategies, packaging).meta-sdk-patterns-eng
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
on newly registered provider before resolutioninitialize - MUST call
on previously registered provider when replacedshutdown - 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