A checkout rollout needs to select the new flow without removing the old one.
A feature flag supplies that decision, or a scalar setting, for a particular
subject. Your application reads flags through *feature.Flags; the selected
driver evaluates values managed in memory or in a provider’s tools.
The portable contract does not create flags or change provider rules.
Flags are not authorization. Enforce account permissions regardless of which flow a flag selects, including when evaluation fails.
Flag Decisions
Choose the safe behavior before declaring a flag. A feature.Flag[T] binds the
literal provider key, Go value type, and fallback in one immutable definition.
Keep the definition in the application package that owns the behavior and share
it with tests. Import feature from arkoren.dev/core/service/feature:
var expressCheckout = feature.NewFlag("checkout-express", false)
This definition does not register a provider flag. Configure that exact key in
your provider; its schema is still checked at evaluation time. Empty names and
zero definitions are invalid: Err() reports feature.ErrInvalidFlag without
contacting a driver.
With an existing configured facade flags, operation context ctx, and stable
account ID accountID, read the decision:
subject := feature.Subject{Key: accountID}
enabled, err := flags.Value(ctx, expressCheckout, subject)
Use enabled to select the checkout flow, after permission checks. Every non-nil
error comes with the declared fallback, so this flag selects the old flow on
failure. Focused snippets keep error results visible but omit routine propagation;
complete application code still handles its errors.
The same fallback can mean several different things:
| Evaluation | Value | Error |
|---|---|---|
| Disabled flag | Declared fallback | Nil |
| No value applies to this subject | Declared fallback | Nil |
| Missing provider key | Declared fallback | feature.ErrNotFound |
| Incompatible value or numeric conversion | Declared fallback | feature.ErrTypeMismatch |
| Driver still loading | Declared fallback | feature.ErrNotReady |
| Invalid subject or definition | Declared fallback | feature.ErrInvalidSubject or feature.ErrInvalidFlag |
Errors wrap these identities; classify them with standard-library errors.Is.
Cancellation and other provider errors also return the fallback. A successful
provider result can itself equal the fallback, so the value alone does not prove
whether evaluation succeeded. Test your application’s
failed-evaluation policy explicitly.
Scalar Settings
Use a value-bearing flag when a rollout needs more than a boolean. Definitions support booleans, strings, signed integers, and floating-point values, including named types:
type CheckoutLayout string
var checkoutLayout = feature.NewFlag("checkout-layout", CheckoutLayout("standard"))
layout, err := flags.Value(ctx, checkoutLayout, subject)
layout retains the CheckoutLayout type. JSON objects and unsigned integers
are not supported. Integer values can be read as floats; float values can be read
as integers only when whole and representable. Reading 3.5 as an integer, or
300 as int8, returns the fallback with ErrTypeMismatch, not a truncated value.
Subject Targeting
A subject identifies who the decision is for and supplies facts that provider rules can match. Build those facts from trusted application state rather than letting request fields choose account identity or permissions. For an account already loaded by your application, use a small typed input:
type CheckoutAccount struct {
ID string
Plan string
Seats int64
Beta bool
}
Convert an existing account CheckoutAccount into the contract’s attribute map:
subject := feature.Subject{
Key: account.ID,
Attributes: map[string]any{
"plan": account.Plan,
"seats": account.Seats,
"beta": account.Beta,
},
}
enabled, err := flags.Value(ctx, expressCheckout, subject)
The facade accepts only string, bool, int64, float64, []string, and
time.Time attribute values. A plain int is invalid; convert it explicitly.
Invalid attributes fail before driver access, and validation errors name the
attribute rather than its value. Do not mutate the map or its slices during use.
Send only facts approved for the provider to receive.
Keep subject keys stable when your provider uses them for rollout bucketing. Arkoren does not guarantee identical buckets across providers, unchanged assignments after rule changes, exact percentages in a population, or atomic rollout changes across replicas. A zero subject is valid and anonymous; provider handling differs, especially for LaunchDarkly.
Segments and targeting rules remain provider-owned. A subject key identifies the
evaluated entity, not a segment; attributes supply facts, not portable segment
membership instructions. The memory driver supports key overrides but ignores
attributes. The subject is always an explicit argument, not inferred from ctx.
Evaluation Details
When investigating an unexpected flow, read the value and its explanation in one evaluation rather than evaluating again for diagnostics:
detail, err := flags.ValueDetail(ctx, expressCheckout, subject)
detail.Value has the same fallback behavior as Value. detail.Reason describes
the driver’s choice: static value, targeting match, split, default, disabled,
cached, or unknown. On failure it is ReasonError, the variant is empty, and the
error remains available separately. A no-value success preserves the driver’s
reason and variant while substituting the fallback.
detail.Variant is optional provider metadata, not the flag key or subject ID.
OpenFeature passes through a provider’s variant; LaunchDarkly reports a variation
index as text. Reason coverage differs too: not every percentage rollout reports
ReasonSplit. Use these fields for diagnostics, not authorization or a portable
business branch. Drivers explains each mapping.
Flag Namespaces
If several flags share a key prefix and the same typed fallback, a namespace keeps that naming policy together:
checkoutFlags := feature.NewNamespace("checkout", false)
express := checkoutFlags.Flag("express")
express.Name() is checkout:express, and its fallback is false. Each child
inherits the exact fallback and Go type; use another namespace or standalone
definition when either differs. Create the assembled key in the provider.
Namespaces add neither provider isolation nor targeting or authorization.
If the provider uses dotted keys, derive a separate naming policy:
dotted := checkoutFlags.Derive(func(names *feature.NamespaceBuilder) {
names.Separator(".")
})
refunds := dotted.Subnamespace("payments").Flag("refunds")
The resulting key is checkout.payments.refunds; the original namespace is
unchanged. A subnamespace joins its own name using the parent’s separator before
applying child callbacks to future joins. An empty separator concatenates names.
Names are literal: no trimming, escaping, or normalization prevents collisions
such as a:b plus c and a plus b:c.
Empty prefixes or child names and zero namespaces retain ErrInvalidFlag through
all descendants. Construction callbacks run once in order, including for invalid
definitions; nil callbacks panic, and retained drafts cannot change published
values. Namespace formatting and structured logging redact names and fallbacks;
explicit Name() and Fallback() accessors expose them.
Prefer stable flag keys with subject targeting over per-account keys. If dynamic keys really exist in the provider, validate or encode components before joining them and format numeric suffixes explicitly. Keys enter telemetry: personal IDs and unbounded names create privacy and cardinality problems.
Raw Requests
Application decisions should use the typed facade. A custom adapter or contract test can call a driver directly with an explicit request:
result, err := flags.Driver().Evaluate(ctx, feature.Request{
Key: expressCheckout.Name(),
Kind: feature.KindBool,
Subject: subject,
})
This bypasses facade validation, fallback conversion, events, and telemetry.
result.Value == nil means no value was supplied; it does not install your
application fallback. Drivers first reject an already-canceled context, then
reject empty or unknown kinds with ErrTypeMismatch before readiness, lookup,
disabled/default shortcuts, or provider I/O. Only KindBool, KindString,
KindInt, and KindFloat are valid. Unknown kind strings can round-trip through
JSON and String() without becoming valid requests.
Events
Fallbacks let requests continue, but repeated provider failures still need
attention. At facade construction, connect an existing *event.Bus named bus
from arkoren.dev/core/event:
flags := feature.New(driver, func(client *feature.Builder) {
client.Events(bus)
})
Each driver evaluation error publishes feature.evaluation.failed with the key
and error. Successful disabled/default results publish nothing. Invalid
definitions, invalid attributes, and conversion errors detected by the facade
also publish nothing; use telemetry to observe those failures.
The payload has no structured subject, attribute, or value fields, but provider error text can contain sensitive information. Classify errors instead of forwarding raw payloads to untrusted destinations. Count or sample hot-path failures rather than alerting once per evaluation. See built-in events for the event contract.
Telemetry
Evaluations follow the shared telemetry handle
unless construction explicitly supplies Builder.Telemetry. The process default
is resolved on each evaluation and is a no-op until configured. Builder.Name
sets a fixed provider label; its default is default.
When enabled, each evaluation records an internal feature evaluate span,
arkoren.feature.evaluations counter, and
arkoren.feature.evaluation.duration histogram. Spans and counters include the
key, provider label, and reason; the histogram includes key and provider label.
Failures add the error class as error.type, never the error text. Nonempty
variants appear on spans only.
Facade telemetry does not record subject keys, attributes, or flag values. Keep keys and provider labels bounded and nonpersonal, and review variant names too. Native SDK hooks and analytics have their own data policy; facade redaction does not sanitize them. Instrumented components lists the metric and attribute contracts.
For every function and option, see the
feature API reference.