AnnouncementsIntroducing Arkoren

    Menu

    Feature Flags Overview

    Evaluate typed flags for explicit subjects and choose safe behavior when evaluation supplies no value or fails.

    What's on this page
    1. Declare typed flag decisions and distinguish successful fallbacks from evaluation failures.
    2. Build explicit targeting subjects without treating flags as account permissions.
    3. Interpret provider details and compose literal keys with shared namespace fallbacks.
    4. Observe failures through events and telemetry while protecting private data and cardinality.
    On this page

    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.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive