ider configuration, return types, or fallback compatibility when the identifier is computed at runtime.
Refactored Implementation:
// Strategy map replaces runtime computation
const TIER_FLAG_MAP: Readonly<Record<string, string>> = Object.freeze({
trial: 'pricing-trial-access',
standard: 'pricing-standard-access',
premium: 'pricing-premium-access',
});
function resolveTierFlag(tier: string): string {
return TIER_FLAG_MAP[tier] ?? 'pricing-trial-access';
}
// Evaluation now uses a statically verifiable key
async function evaluatePricingAccess(tier: string, context: EvaluationContext): Promise<boolean> {
const flagKey = resolveTierFlag(tier);
return featureClient.getBooleanValue(flagKey, false, context);
}
Architectural Rationale: Extracting dynamic resolution into a frozen lookup map converts runtime computation into a compile-time contract. The Readonly and Object.freeze guarantees prevent accidental mutation, while the fallback ensures deterministic behavior. This pattern enables static analysis tools to verify provider configuration and type alignment in subsequent migration passes.
Vendor SDKs expose rich evaluation metadata, but OpenFeature deliberately standardizes the reason vocabulary. Direct consumption of vendor-specific reason codes breaks during migration.
Refactored Implementation:
// Vendor-specific reason mapping layer
const REASON_TRANSLATOR: Record<string, string> = {
RULE_MATCH: 'TARGETING_MATCH',
FALLTHROUGH: 'STATIC',
PREREQUISITE_FAILED: 'ERROR',
};
async function evaluateWithTracking(flagId: string, context: EvaluationContext): Promise<boolean> {
const evaluation = await featureClient.getBooleanDetails(flagId, false, context);
const normalizedReason = REASON_TRANSLATOR[evaluation.reason] ?? evaluation.reason;
if (normalizedReason === 'TARGETING_MATCH') {
analytics.track('flag_targeting_matched', { flagId });
}
return evaluation.value;
}
Architectural Rationale: OpenFeature's ResolutionDetails uses a normalized vocabulary (CACHED, DEFAULT, ERROR, SPLIT, STATIC, TARGETING_MATCH, UNKNOWN). Vendor-specific codes like RULE_MATCH or bigSegmentsStatus do not map 1:1. Introducing a translation layer isolates vendor semantics from business logic, ensuring that telemetry and conditional branching remain stable regardless of the underlying provider.
Phase 3: Replace Bulk State Retrieval
OpenFeature's specification explicitly excludes bulk evaluation. Providers are expected to surface individual flags, and bulk retrieval is treated as a vendor-specific optimization.
Refactored Implementation:
// Explicit enumeration replaces bulk fetch
const BOOTSTRAP_FLAGS = ['ui-dark-mode', 'checkout-flow-v2', 'payment-gateway'] as const;
async function generateFlagSnapshot(context: EvaluationContext): Promise<Record<string, boolean>> {
const evaluations = await Promise.all(
BOOTSTRAP_FLAGS.map(async (flag) => [
flag,
await featureClient.getBooleanValue(flag, false, context),
])
);
return Object.fromEntries(evaluations) as Record<string, boolean>;
}
Architectural Rationale: Enumerating required flags transforms an opaque bulk operation into a deterministic, auditable contract. This approach eliminates provider coupling, improves cacheability, and aligns with OpenFeature's design philosophy. For transitional periods, teams may access the underlying vendor client through the provider interface, but this should be treated as technical debt with a strict deprecation timeline.
Phase 4: Migrate Wrapper Implementations
Shared evaluation helpers abstract vendor SDKs but obscure the migration surface. Rewriting call sites without updating the wrapper internals creates inconsistent evaluation paths.
Refactored Implementation:
// Wrapper migration: internal delegation only
export async function resolveFeatureToggle(
flagKey: string,
context: EvaluationContext,
fallback: boolean
): Promise<boolean> {
// Metrics and logging remain intact
const start = performance.now();
const result = await featureClient.getBooleanValue(flagKey, fallback, context);
const latency = performance.now() - start;
metrics.histogram('flag.evaluation_ms', latency, { flag: flagKey });
return result;
}
Architectural Rationale: Wrappers should be migrated before call sites. The external signature remains unchanged, preserving downstream compatibility, while the internal delegation shifts to OpenFeature. This approach centralizes provider logic, simplifies testing, and ensures that metrics, caching, and error handling remain consistent across the codebase.
Phase 5: Enforce JSON Type Contracts
Untyped JSON fallbacks create ambiguity. OpenFeature returns JsonValue, a union type that requires explicit casting and runtime validation to prevent shape mismatches.
Refactored Implementation:
// Explicit contract with runtime validation
interface RoutingConfig {
primary: string;
fallback: string;
weight: number;
}
const ROUTING_DEFAULT: RoutingConfig = { primary: 'us-east-1', fallback: 'eu-west-1', weight: 0.5 };
async function loadRoutingConfig(context: EvaluationContext): Promise<RoutingConfig> {
const raw = await featureClient.getObjectValue('routing-config', ROUTING_DEFAULT, context);
// Runtime validation prevents shape drift
if (typeof raw?.weight !== 'number' || !raw?.primary) {
logger.warn('Invalid routing config shape, falling back to defaults');
return ROUTING_DEFAULT;
}
return raw as RoutingConfig;
}
Architectural Rationale: JsonValue encompasses primitives, arrays, and nested objects. Blind casting assumes provider consistency, which breaks during dashboard updates or provider swaps. Explicit interfaces combined with lightweight runtime validation ensure that configuration drift is caught at evaluation time rather than manifesting as silent type errors downstream.
Pitfall Guide
1. Assuming Bulk Evaluation Parity
Explanation: Teams often attempt to replicate allFlags() or allFlagsState() using OpenFeature, expecting a direct equivalent. The specification intentionally omits bulk retrieval to maintain provider neutrality.
Fix: Enumerate required flags explicitly or use provider-specific bootstrap mechanisms during transition. Treat bulk retrieval as an architectural anti-pattern in OpenFeature.
2. Migrating Call Sites Before Wrappers
Explanation: Rewriting downstream calls while the wrapper still references the vendor SDK creates dual evaluation paths. This leads to inconsistent flag states, duplicated metrics, and broken caching layers.
Fix: Update wrapper implementations first. Verify that the wrapper delegates to OpenFeature before touching call sites. Run integration tests to confirm single-path evaluation.
3. Ignoring Reason Code Vocabulary Differences
Explanation: Vendor SDKs expose granular evaluation reasons (RULE_MATCH, PREREQUISITE_KEY, bigSegmentsStatus). OpenFeature normalizes these into a smaller set. Direct consumption breaks telemetry and conditional branching.
Fix: Implement a reason translation layer. Map vendor codes to OpenFeature equivalents and add unit tests that verify each translation path.
4. Blindly Casting JsonValue Without Validation
Explanation: getObjectValue() returns a union type. Assuming the provider always returns the expected shape leads to runtime type errors when dashboard configurations change or providers are swapped.
Fix: Define explicit interfaces for JSON payloads. Add lightweight runtime validation (type guards or schema validators) at the evaluation boundary.
5. Skipping the Audit Phase
Explanation: Running migrate --apply without a prior audit masks high-risk patterns. Teams miss dynamic keys, bulk calls, and wrapper dependencies until production incidents occur.
Fix: Always run flaglint audit ./src first. Categorize findings by risk level. Resolve high-risk patterns manually before enabling automated rewrites.
6. Treating Dynamic Keys as Configuration
Explanation: Developers often embed flag key computation inside business logic, assuming it's a configuration concern. This obscures evaluation contracts and prevents static verification.
Fix: Extract dynamic resolution into dedicated strategy modules or lookup maps. Treat flag keys as code contracts, not runtime variables.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Known flag set for client hydration | Explicit enumeration with Promise.all | Aligns with OpenFeature spec, improves cacheability, eliminates provider coupling | Low (initial refactoring) |
| Transitional period with legacy bulk consumers | Provider bypass via internal client access | Maintains compatibility while migrating evaluation paths | Medium (technical debt, requires deprecation plan) |
| Complex JSON configuration payloads | Explicit interface + runtime validation | Prevents shape drift, catches dashboard updates early | Low (validation overhead) |
| High-volume dynamic key resolution | Frozen lookup map + strategy resolver | Enables static analysis, guarantees type safety | Low (map maintenance) |
| Telemetry dependent on evaluation reasons | Reason translation layer + normalized tracking | Decouples business logic from vendor semantics | Medium (mapping maintenance) |
Configuration Template
{
"wrappers": [
"resolveFeatureToggle",
"evaluateWithTracking",
"loadRoutingConfig"
],
"staticKeys": {
"pricing-trial-access": "boolean",
"checkout-flow-v2": "boolean",
"routing-config": "object"
},
"reasonMapping": {
"RULE_MATCH": "TARGETING_MATCH",
"FALLTHROUGH": "STATIC",
"PREREQUISITE_FAILED": "ERROR"
},
"bootstrapFlags": [
"ui-dark-mode",
"checkout-flow-v2",
"payment-gateway"
]
}
Quick Start Guide
- Initialize audit: Run
flaglint audit ./src to generate a risk breakdown of all evaluation calls.
- Configure wrappers: Add shared evaluation helpers to
.flaglintrc under the wrappers key to surface them in scan reports.
- Refactor high-risk patterns: Address dynamic keys, bulk calls, and wrapper internals using the patterns outlined in Core Solution.
- Validate dry run: Execute
flaglint migrate ./src --dry-run to confirm automatable calls and review remaining manual flags.
- Apply migration: Run
flaglint migrate ./src --apply once high-risk patterns are resolved, then run integration tests to verify evaluation consistency.