Skip to content

Evaluate dynamic flags through a backend

Opt a feature in

Only a descriptor that reports IsDynamic() may be overridden at evaluation time. Everything else is answered from the Set, whatever a backend says. Keep the features that shape a command tree, a help screen or a generated artefact static; a request-time decision inside a running service is what a dynamic feature is for.

Build the evaluator

flags := features.Dynamic(set, backend,
    features.WithInitTimeout(2*time.Second),
    features.WithOnFallback(func(id features.ID, err error) {
        logger.Warn("flag fell back to the static state", "feature", id, "error", err)
    }),
)

Dynamic fixes the set of dynamic IDs at construction, so evaluating a static-only feature is one map lookup before the backend is considered.

Lifecycle

if err := flags.Init(ctx); err != nil {
    // The evaluator is usable regardless: it answers with fallbacks until Ready.
}
defer flags.Close()

for change := range flags.Watch(ctx) {
    // A backend that pushes updates says a flag (or everything) may have moved;
    // the next Evaluate sees it. Nil channel when the backend does not push.
}

A short-lived process gives Init a short WithInitTimeout and lives with fallbacks; a long-lived one gives it none and ties Init and Close to its own lifecycle.

Evaluate

dec, err := flags.Evaluate(ctx, "beta-ranking", features.EvalContext{
    TargetingKey: userID,
    Attributes:   map[string]any{"tier": tier},
})

Decision.Reason says how the answer was reached:

Reason Meaning err
static a static-only feature, or a Set answered nil
default the backend's default rule nil
targeting the backend matched the EvalContext nil
disabled the backend has the flag disabled; the static state applies nil
fallback the backend errored, was not ready, or did not know the flag; the static state applies the cause
error no such feature ErrUnknownFeature

The static state is the fallback for every failure. It is the tool's own decision (its manifest, its config), made once, which is the only place an offline default can honestly live. Variant is empty for a plain toggle; a multivariate backend fills it.

With no backend yet

features.SetBackend(set) is a Backend that answers statically, and a Set is itself an Evaluator. Wire a service against Evaluator today and swap a Dynamic in later without touching a call site.