OpenFeature vs Vendor SDK Trade-offs
This how-to is part of Choosing a Flag Evaluation Strategy. You are standing up feature flags in a new service and have to pick between the vendor-neutral OpenFeature API and the flag vendor’s own native SDK. The choice looks small — both resolve a boolean in one line — but it decides how much it costs to change vendors later, which advanced features you can reach, and how much abstraction you carry forever.
Treat this as an architectural decision, not a library preference. The line of code you write today gets copied into hundreds of call sites over the next two years, and the shape of that line — whether it names a vendor or not — is what you are really choosing. A native SDK call is cheap to write and expensive to unwind; an OpenFeature call is one import heavier but leaves the vendor identity in exactly one place. The right answer is rarely dogmatic. It depends on how many vendors you might run, how deeply you lean on one vendor’s proprietary features, and how honestly you can forecast a migration you have every incentive to believe will never happen.
Prerequisites
Step-by-Step Procedure
Step 1 — Prototype the same read through both APIs
Write the identical flag read twice — once through OpenFeature, once through the native SDK — and compare what each requires. The OpenFeature version imports no vendor type; the native version imports the vendor’s client and context classes. That import list is the tell: every vendor symbol that appears in application code is a thread you will have to cut during a migration, and the prototype makes those threads visible before you have written a hundred of them.
Look past the happy path when you compare. Note how each API models the evaluation reason and variant — OpenFeature returns a typed EvaluationDetails with a reason field (TARGETING_MATCH, DEFAULT, ERROR), while native SDKs often bury the same signal in a vendor-shaped detail object. Note how each handles a missing flag or an unreachable backend: OpenFeature guarantees the default value is returned and an error reason is set, so a flag read can never throw into your request path. If the two APIs disagree on default-on-error behavior, that difference will shape your error handling everywhere, so it belongs in the prototype, not in an incident.
// openfeature.ts — portable; no vendor name in application code
import { OpenFeature } from '@openfeature/server-sdk';
const client = OpenFeature.getClient();
const on = await client.getBooleanValue('web.checkout.express-pay', false, ctx);
// vendor.ts — direct; couples this file to the vendor's types
import { VendorClient, VendorContext } from '@vendor/sdk';
const vClient = new VendorClient(process.env.VENDOR_KEY!);
const on2 = await vClient.boolVariation('web.checkout.express-pay', new VendorContext(ctx), false);
Step 2 — Map your relied-upon features to provider support
For each native feature you use, check whether the OpenFeature provider surfaces it — through standard evaluation, provider metadata, or a documented extension. Features with no mapping are the real cost of portability. Be precise about what “supported” means: a feature is genuinely covered only if it works through the neutral API with no vendor import, resolved inside the provider where your code never sees it. A feature that technically works but requires you to reach past OpenFeature into the underlying client is not portable — it is a leak waiting to spread.
The categories in the inventory below matter more than the count. standard features move with you to any conformant provider for free. provider-side features — prerequisite flags, segment resolution, percentage rollouts — are computed by the provider and handed back as a plain value, so they are portable in effect even though the logic is vendor-specific. native-only is the dangerous bucket: a proprietary experiment dashboard or a bespoke targeting builder that has no neutral representation. If your relied-upon list is mostly the first two categories, OpenFeature costs you almost nothing; if a native-only feature is load-bearing, that single row can justify the whole native SDK.
// feature-map.ts — a candid inventory drives the decision
const featureSupport = {
'boolean/string/number eval': 'standard', // covered by OpenFeature
'evaluation reason + variant': 'standard', // via EvaluationDetails
'prerequisite flags': 'provider-side', // resolved in provider, transparent
'vendor autoexperiment UI': 'native-only', // no OpenFeature equivalent
};
Step 3 — Wrap any vendor-specific need behind your own interface
If you need a native-only feature but still want reversibility, put the vendor call inside a custom OpenFeature provider or a thin internal module — never inline in business logic. The commitment then lives in one file you can rewrite during a migration.
The discipline that makes this work is a domain-shaped interface, not a vendor-shaped one. Export trackExposure(key, ctx), not getVendorExperimentClient() — the moment a vendor object escapes the wrapper, every caller that touches it becomes a migration site again. Name the module for what it does (experiments.ts, targeting.ts), keep its public signature free of vendor types, and enforce the boundary with a lint rule or a grep in CI so an import of @vendor/sdk outside that one file fails the build. This is the same reversibility you get from OpenFeature’s provider layer, applied by hand to the one feature that could not travel through the neutral API.
// experiments.ts — the ONLY file that names the vendor
import { VendorClient } from '@vendor/sdk';
const vendor = new VendorClient(process.env.VENDOR_KEY!);
export const trackExposure = (key: string, ctx: Ctx) => vendor.track(key, ctx);
Step 4 — Estimate the migration cost each way
Count the files that would change if you swapped vendors. Under OpenFeature it is one — the provider registration. Under a native SDK it is every file that imported a vendor type. That count is the portability premium made concrete.
Do not stop at the file count, because a migration is more than a find-and-replace. With OpenFeature the swap is a provider registration change plus a re-verification pass — the evaluation contract is fixed, so behavior that passed before should pass after. With a native SDK you also inherit the semantic gaps between vendors: one models a context as a flat attribute bag, another as a nested user object; one returns undefined for an unknown flag, another throws. Those mismatches are invisible in a file count but they are where migration weeks actually go. When you present the premium to a team, pair the grep number with a short note on which behaviors would have to be re-derived, so nobody reads “dozens of files” as “dozens of trivial edits.”
# how many files name the vendor today?
grep -rl "@vendor/sdk" src/ | wc -l # native SDK: this many files change on migration
grep -rl "@openfeature" src/ | wc -l # OpenFeature: application files stay put
Verification
Confirm the abstraction actually holds by swapping the provider for an in-memory one in a test and running the suite unchanged. If application code needs no edits, portability is real; if it breaks, a vendor type has leaked. The in-memory provider that ships with OpenFeature is ideal here precisely because it shares nothing with your real vendor — any coupling that survived review will surface as a compile error or a failing assertion rather than as a quiet production dependency.
Run the leak check as a standing gate, not a one-time proof. A codebase that is portable today drifts closed the first time someone reaches for a vendor-only convenience under deadline, and the abstraction erodes one commit at a time. Keep the provider-swap test in CI and back it with a source scan — grep -rl "@vendor/sdk" src/ should return only your wrapper module, and any other hit fails the build. Portability is a property you maintain, not a checkbox you tick at setup.
# Swap to the in-memory provider and run the suite — no app-code changes should be needed
OPENFEATURE_PROVIDER=in-memory npm test
# A green run proves application code is provider-agnostic.
Gotchas & Edge Cases
- Leaky abstraction defeats the point. If OpenFeature reads are sprinkled with occasional direct vendor calls, you have paid for the abstraction and kept the lock-in. Keep every vendor call in one wrapper module, and enforce it — a lint rule or CI
grepthat fails on a stray vendor import is worth more than a coding convention nobody rereads. - Feature parity is not guaranteed. A vendor feature with no OpenFeature equivalent — a bespoke experiment UI, a proprietary segment builder — cannot be reached through the neutral API. Decide up front whether you can live within the standard surface, and remember the surface grows over time: a feature unmapped this year may land in a later spec revision or a provider extension, so recheck before you rebuild it by hand.
- The provider layer has a cost, usually tiny. For local in-process evaluation the extra indirection is negligible; if you are unsure, benchmark it before assuming. See local vs remote evaluation latency.
- Context shape is the real portability boundary. OpenFeature standardizes the call, but each provider maps your evaluation context onto vendor-specific targeting attributes. If you rename a targeting key or nest an attribute to please one vendor, that shape can quietly become part of your contract — enrich context in a shared helper, not ad hoc at each call site, so a provider swap does not silently change who matches a rule. See context enrichment strategies.
- Initialization and readiness differ across providers. Some providers resolve flags synchronously from a local cache; others need an async
setProviderAndWaitbefore the first read is trustworthy. Application code that assumes one model breaks against the other, so gate first reads on the OpenFeatureREADYevent rather than on a provider-specific ready flag — that keeps the startup path portable too.
Troubleshooting & FAQ
If OpenFeature is portable, why do vendors ship native SDKs at all?
Native SDKs expose the vendor’s full surface — experiment analysis, proprietary targeting builders, telemetry — some of which the standard OpenFeature API does not model. If those features are core to your workflow, the native SDK is the pragmatic choice; wrap it so the choice stays reversible.
Can I use OpenFeature and still get vendor-specific telemetry?
Yes, through hooks. OpenFeature’s hook API lets you emit exposure events to the vendor’s analytics on every evaluation without naming the vendor in business logic. The hook is the one place the vendor is referenced.
Does OpenFeature lock me into its own ecosystem instead?
OpenFeature is a CNCF specification with multiple independent implementations, not a product. Standardizing on it couples you to an open API surface rather than to any single company’s SDK.
Should a single-vendor shop still bother with OpenFeature?
Often yes, but not always. Even with one vendor you gain a stable evaluation contract, a free in-memory provider for tests, and a clean seam if that vendor’s pricing or reliability forces a change later. The layer earns its keep whenever a future swap is plausible; if you are certain the vendor is permanent and you lean hard on its native-only features, the direct SDK is the honest choice — just keep the calls wrapped.
Can I run OpenFeature and a native SDK side by side during a migration?
Yes, and it is the usual path. Register the new vendor as an OpenFeature provider while the old one still runs behind its native calls, then move call sites over a flag key at a time and compare results. Because the neutral API returns an evaluation reason, you can shadow-read both and alert on divergence before you cut traffic. Retire the native path only once the source scan shows no remaining vendor imports outside the wrapper.
How do I handle a flag type OpenFeature does not model natively?
OpenFeature covers boolean, string, number, and structured object values, which spans almost every flag you will write. For a genuinely bespoke payload, resolve it as an object value and validate its schema in your wrapper, or expose a typed accessor over the object result. Reaching into the native client for a custom type reintroduces coupling, so treat it as a last resort and keep it inside the one vendor-aware module.