Choose memory for deterministic application tests or process-local settings. Choose OpenFeature when your flag service has an OpenFeature Go provider or your application already manages an OpenFeature client. Choose the native LaunchDarkly driver when you want direct integration with LaunchDarkly’s official server SDK. All three expose the same typed evaluation, but targeting rules, update delivery, and outage behavior are not interchangeable.
Compatibility Matrix
This comparison covers Arkoren’s exposed evaluation contract, not provider flag administration. Scalar flags are booleans, strings, signed integers, and floats. Every driver accepts standalone and namespace-derived definitions; neither form creates provider keys or enforces a provider schema.
| Driver | Scalar Flags | Key Targeting | Attribute Targeting | Object Flags | Native Client | Remote-Free Evaluation |
|---|---|---|---|---|---|---|
| Memory | Yes | No | No | No | Yes | |
| OpenFeature | Yes | No | Yes | |||
| LaunchDarkly | Yes | No | Yes |
No driver guarantees atomic rule changes across processes or identical rollout assignments across providers. Memory has independent state in each instance. OpenFeature’s provider determines remote availability requirements. LaunchDarkly can evaluate its last received data during a connection outage, but that does not prove the data is current or let a new process start without initial data.
Run installation commands from your application’s Go module. Examples use
feature from arkoren.dev/core/service/feature; ctx is an operation context.
The facade itself has no lifecycle. Register driver startup, cleanup, and
readiness with your existing application lifecycle,
and do not evaluate an owned provider until startup succeeds.
Memory
memoryfeature is included in arkoren.dev/core and needs no external service.
Use it when your application controls the stored values and needs neither
provider targeting rules nor shared state between processes.
import "arkoren.dev/core/service/feature/memoryfeature"
driver := memoryfeature.New()
flags := feature.New(driver)
Construction performs no I/O, has no options, and starts no background work.
There are no Start, Stop, or Ping hooks to register.
To supply the checkout decision, share the application’s typed definition:
expressCheckout := feature.NewFlag("checkout-express", false)
err := driver.Set(expressCheckout, true)
The next facade evaluation returns true unless
the flag is disabled or a subject override selects another value. Setup errors
must be handled before the value is used. Set retains existing overrides and
disabled state; it does not install the definition’s fallback as a stored value.
Testing covers overrides and state changes.
The driver is concurrent-safe and loses all state when discarded. It ignores subject attributes, has no rollout engine, and never matches an override whose key is empty. Separately constructed definitions with the same provider name still address the same stored entry; they do not establish a schema.
See the memoryfeature API.
OpenFeature
OpenFeature connects an evaluation API to a provider. Supply an
openfeature.FeatureProvider from your chosen vendor’s Go integration, configured
with that vendor’s endpoint and credentials. Provider documentation determines
targeting support, refresh intervals, network calls, caching, and outage recovery;
Arkoren does not add missing capabilities.
Provider Setup
Install the Arkoren module and the Go provider module you selected:
go get arkoren.dev/driver/openfeature
With the configured provider named provider, construct an owned driver:
import "arkoren.dev/driver/openfeature/openfeaturefeature"
driver := openfeaturefeature.New(provider)
flags := feature.New(driver)
New performs no I/O. Start registers and initializes the provider in a private
OpenFeature API instance; it neither uses nor replaces the global provider.
One provider belongs to one owned driver. Register Stop for cleanup, Start
for startup, and Ping for readiness. Ping performs no network call and
requires the SDK’s provider state to be READY; a stale provider is not ready
even if it can still serve a cached evaluation.
An owned driver cannot restart after Stop or failed Start; construct a new
one to retry initialization. Stopping before startup also makes it terminal,
without initializing or shutting down the unused provider. Failed startup
attempts provider cleanup and leaves evaluation unavailable.
Owned lifecycle calls serialize, and waiting for another lifecycle call is not
bounded by ctx. Context-aware providers must cooperate with initialization and
cleanup cancellation. Legacy contextless shutdown is not bounded by the cleanup
context, and a legacy initializer can outlive canceled startup inside the SDK.
Do not call the same driver’s lifecycle recursively from provider callbacks.
Ambient Context
If every rule needs a deployment fact, supply it once at construction. Import
openfeature from github.com/open-feature/go-sdk/openfeature:
driver := openfeaturefeature.New(provider, func(features *openfeaturefeature.Builder) {
features.EvaluationContext(openfeature.NewTargetlessEvaluationContext(
map[string]any{"environment": "production"},
))
})
This is an alternative construction. Per-evaluation subject attributes replace
ambient attributes of the same name; a nonempty subject key replaces the ambient
targeting key. An empty key leaves the ambient key in effect. If neither provides
a key, providers that require one report feature.ErrInvalidSubject.
Construction copies outer attribute maps at input and publication, not nested values. Retained drafts cannot change the published context; keep shared nested values immutable. Construction callbacks run once in order; nil callbacks panic. Configure credentials, polling, and network budgets on the provider, not on the Arkoren builder.
Evaluation Results
After startup, use the shared flag decision. The subject key becomes the targeting key, and accepted attributes pass through unchanged. Each provider decides which types its rules support. Provider-side segments retain that provider’s own identifiers and rules.
Missing flags map to ErrNotFound; type and parse errors to ErrTypeMismatch;
not-ready providers to ErrNotReady; missing targeting keys and invalid contexts
to ErrInvalidSubject. Other failures retain the provider error code in the
error. A reported disabled result supplies no value, so the facade uses the
definition’s fallback without an error.
Known reasons map to their portable equivalents; STALE maps to ReasonCached.
Empty or unrecognized reasons become ReasonUnknown. Variants pass through
unchanged. An OpenFeature default result can include a real provider value:
ReasonDefault alone does not mean the application fallback was used.
Native Ownership
If your application already registers an OpenFeature provider and manages its
hooks, domains, and shutdown, borrow its initialized *openfeature.Client
named client:
driver := openfeaturefeature.NewFrom(client)
flags := feature.New(driver)
NewFrom accepts no options. Start and Stop do nothing; the application
retains provider registration and cleanup. Ping still reports provider state.
Do not rebind or shut down a New driver’s owned provider through native access.
To attach an existing openfeature.Hook named hook during setup, use the native
client before concurrent evaluation:
driver.Client().AddHooks(hook)
Hooks can receive subject data and values omitted by facade telemetry. Review
their export policy before use. The owned native client is available before
startup, but direct evaluation then returns SDK no-op results rather than the
facade’s ErrNotReady. Native calls bypass facade validation and telemetry.
Native access does not add portable object flags,
and Arkoren ships no OpenFeature flag-administration or webhook adapter.
Local Provider
The SDK’s memory provider exercises the OpenFeature path without a remote service. Define the fixture separately from driver construction:
import "github.com/open-feature/go-sdk/openfeature/memprovider"
provider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{
"checkout-express": {
Key: "checkout-express",
State: memprovider.Enabled,
DefaultVariant: "express",
Variants: map[string]any{"express": true},
},
})
Pass this provider to New and run the lifecycle described above. After startup,
evaluating feature.NewFlag("checkout-express", false) returns true with no
error. Stop releases the provider; no server needs cleanup. This proves SDK
wiring, not your production provider’s targeting or refresh behavior.
See the openfeaturefeature API.
LaunchDarkly
Choose this driver for LaunchDarkly’s official Go server SDK. The SDK receives flag updates and evaluates locally. You need a LaunchDarkly environment, its server-side SDK key, and the application’s flags configured in that environment. Load the key from approved secret configuration rather than embedding it in code.
Provider Setup
go get arkoren.dev/driver/launchdarkly
With the environment’s key already loaded as sdkKey, construct an owned driver:
import "arkoren.dev/driver/launchdarkly/launchdarklyfeature"
driver, err := launchdarklyfeature.New(sdkKey)
New performs no I/O and rejects an empty key. Handle that error before
constructing the facade:
flags := feature.New(driver)
Register Start, Stop, and Ping. Startup builds the SDK client and waits for
initial flag data; failure closes the acquired client instead of publishing it.
The default SDK receives updates from LaunchDarkly; an SDK-configured persistent
store can provide last-known data. Treat that data as potentially stale, not as
proof of a live remote connection.
Ping performs no I/O and checks that a client exists and reports initialized.
It is not a remote connection or freshness check. Before startup and after stop,
evaluation returns ErrNotReady.
Unlike OpenFeature, an owned LaunchDarkly driver can retry failed startup or
start again after stop. Starting an already running driver fails. Stop closes
the client and flushes pending analytics; the SDK’s close is not bounded by the
cleanup context. Owned lifecycle calls serialize without a context-bounded wait.
Native construction hooks must not recursively call the driver’s lifecycle.
Startup Budget
Startup waits five seconds by default. If your deployment needs a longer initial
synchronization budget, configure it when constructing the driver. Import
standard-library time:
driver, err := launchdarklyfeature.New(sdkKey, func(features *launchdarklyfeature.Builder) {
features.StartWait(10 * time.Second)
})
Zero selects the five-second default; a negative wait returns immediately and
fails unless the source is already ready. Startup checks ctx before client
construction, but subsequent cancellation does not interrupt the SDK’s wait.
StartWait is not an evaluation timeout.
For native endpoints, storage, event settings, or network budgets, supply an SDK hook. This alternative construction disables SDK analytics, not facade telemetry:
import (
ld "github.com/launchdarkly/go-server-sdk/v7"
"github.com/launchdarkly/go-server-sdk/v7/ldcomponents"
)
driver, err := launchdarklyfeature.New(sdkKey, func(features *launchdarklyfeature.Builder) {
features.SDK(func(config *ld.Config) {
config.Events = ldcomponents.NoEvents()
})
})
The hook runs at Start, not at construction. Published settings are detached
from the construction draft, but native objects and captured state remain shared.
All construction callbacks run once in order; nil callbacks panic.
Evaluation Results
Create checkout-express as a boolean flag in the selected environment, then use
the shared flag decision after startup.
An off flag or failed prerequisite returns the definition’s fallback, not LaunchDarkly’s configured off variation. Choose a safe application fallback. If a configured provider value must be served, keep the flag on with its prerequisites satisfied and select that value through targeting rules.
Each subject becomes a context of kind user. An empty key becomes the anonymous
key anonymous, so all anonymous subjects share one identity and rollout bucket.
Attributes named key or kind cannot replace this identity. String lists become
arrays, and time.Time becomes Unix milliseconds for date rules. The SDK can
reject reserved attributes with incompatible types as ErrInvalidSubject.
Target and rule matches report ReasonTargetingMatch; fallthrough reports
ReasonDefault. Experiment rule matches and fallthrough report ReasonSplit;
ordinary percentage rollouts need not. Variants are variation indexes formatted
as strings, such as "1", not named segments or variation labels.
Missing keys, wrong types, uninitialized clients, and invalid contexts map to the corresponding portable errors. Malformed flags and SDK exceptions retain their error kind. Null variations supply no value and therefore use the fallback. Fractional numbers requested as integers fail rather than truncate. Evaluation is local and checks cancellation only before SDK evaluation.
Native Ownership
If your application already owns a configured *ld.LDClient named client,
wrap it with launchdarklyfeature.NewFrom(client). The wrapper accepts no
options, and its Start and Stop do nothing. You must close the client after
all users finish, including any client returned alongside an initialization
error. An owned driver’s Client() is nil before successful startup and after
stop; do not close its client yourself.
For custom analytics, first configure the matching metric in LaunchDarkly. After successful startup, track the metric’s event name with the same stable account identity used for evaluation:
import "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
err := driver.Client().TrackEvent("checkout-completed", ldcontext.New(accountID))
A nil error confirms SDK acceptance, not remote delivery. Analytics can transmit context data outside facade telemetry’s redaction policy; review permitted attributes and event configuration before enabling it. Arkoren ships no native flag-administration or webhook adapter.
Local Data
The SDK’s test data source exercises local evaluation without a LaunchDarkly connection. Using the SDK imports above, define a checkout fixture:
import "github.com/launchdarkly/go-server-sdk/v7/testhelpers/ldtestdata"
data := ldtestdata.DataSource()
data.Update(data.Flag("checkout-express").VariationForAll(true))
Build a client with remote analytics disabled. Retain cleanup even if client initialization fails:
client, err := ld.MakeCustomClient("local-checkout", ld.Config{
DataSource: data,
Events: ldcomponents.NoEvents(),
}, 0)
if client != nil {
defer client.Close()
}
Handle err before wrapping the client with NewFrom and constructing the
facade. Evaluating feature.NewFlag("checkout-express", false) then returns
true without a remote service. The deferred close is yours because the client
is borrowed; there is no server to remove. Use memory fixtures
when only application behavior, rather than SDK integration, is under test.
See the launchdarklyfeature API.