Feature Flags
Rule-based evaluation with tenant overrides and type-safe access.
The flag package provides a complete feature flag system with three layers: Definition (what the flag is), Engine (how it evaluates), and Service (type-safe access for application code). It supports boolean, string, integer, float, and JSON flag types, with targeting rules, per-tenant overrides, and an in-memory evaluation cache.
Architecture
flag.Service (type-safe accessors: Bool, String, Int, Float, JSON)
└── flag.Engine (evaluation logic: disabled -> override -> rules -> default)
├── flag.Store (definitions, rules, overrides)
└── evaluationCache (optional TTL cache)The three layers separate concerns:
- Definition -- The data model for flags, rules, variants, and overrides.
- Engine -- The evaluation engine that resolves a flag key to a concrete value.
- Service -- A thin wrapper providing type-safe accessors with built-in fallback defaults.
Installation
import "github.com/xraph/vault/flag"Flag types
| Type | Constant | Go type | Description |
|---|---|---|---|
| Boolean | flag.TypeBool | bool | On/off toggles |
| String | flag.TypeString | string | Feature variants, experiment names |
| Integer | flag.TypeInt | int | Numeric limits, counts |
| Float | flag.TypeFloat | float64 | Percentages, thresholds |
| JSON | flag.TypeJSON | any | Complex configuration objects |
Definition
A Definition represents the static configuration of a feature flag.
type Definition struct {
vault.Entity
ID id.ID `json:"id"`
Key string `json:"key"`
Type Type `json:"type"`
DefaultValue any `json:"default_value"`
Description string `json:"description"`
Tags []string `json:"tags,omitempty"`
Variants []Variant `json:"variants,omitempty"`
Enabled bool `json:"enabled"`
AppID string `json:"app_id"`
Metadata map[string]string `json:"metadata,omitempty"`
}| Field | Type | Description |
|---|---|---|
Key | string | Unique flag identifier within an app |
Type | Type | One of TypeBool, TypeString, TypeInt, TypeFloat, TypeJSON |
DefaultValue | any | Value returned when no rules or overrides match |
Enabled | bool | When false, the engine always returns DefaultValue |
Tags | []string | Organizational tags for filtering |
Variants | []Variant | Named value variants with descriptions |
Variant
type Variant struct {
Value any `json:"value"`
Description string `json:"description"`
}Targeting rules
Rules are evaluated in priority order (lower number = higher priority). The first matching rule determines the return value. If no rule matches, the DefaultValue is used.
Rule types
| RuleType | Constant | Description |
|---|---|---|
when_tenant | RuleWhenTenant | Matches specific tenant IDs |
when_tenant_tag | RuleWhenTenantTag | Matches tenants with a specific tag key/value |
when_user | RuleWhenUser | Matches specific user IDs |
rollout | RuleRollout | Percentage-based rollout (0--100) |
schedule | RuleSchedule | Time-window activation |
custom | RuleCustom | Delegates to a named plugin evaluator |
Rule struct
type Rule struct {
vault.Entity
ID id.ID `json:"id"`
FlagKey string `json:"flag_key"`
AppID string `json:"app_id"`
Priority int `json:"priority"` // lower = higher priority
Type RuleType `json:"type"`
Config RuleConfig `json:"config"`
ReturnValue any `json:"return_value"`
}RuleConfig
Each rule type uses specific fields within RuleConfig:
type RuleConfig struct {
TenantIDs []string `json:"tenant_ids,omitempty"` // when_tenant
TagKey string `json:"tag_key,omitempty"` // when_tenant_tag
TagValue string `json:"tag_value,omitempty"` // when_tenant_tag
UserIDs []string `json:"user_ids,omitempty"` // when_user
Percentage int `json:"percentage,omitempty"` // rollout (0-100)
StartAt *time.Time `json:"start_at,omitempty"` // schedule
EndAt *time.Time `json:"end_at,omitempty"` // schedule
Evaluator string `json:"evaluator,omitempty"` // custom (plugin name)
Params map[string]any `json:"params,omitempty"` // custom
}Rule constructors
The package provides convenience constructors for each rule type. Chain .Return(value) to set the return value.
// Match specific tenants.
rule := flag.WhenTenant("tenant-a", "tenant-b").Return(true)
// Match tenants with a specific tag.
rule := flag.WhenTenantTag("plan", "enterprise").Return("advanced")
// Match specific users.
rule := flag.WhenUser("user-123", "user-456").Return(true)
// Percentage rollout (deterministic hash-based).
rule := flag.Rollout(25).Return(true) // 25% of tenants
// Time-window schedule.
rule := flag.Schedule(startTime, endTime).Return(true)The Return method is chainable and sets the value returned when the rule matches:
func (r *Rule) Return(value any) *RuleTenantOverride
A TenantOverride bypasses all rules and returns a specific value for a given tenant.
type TenantOverride struct {
vault.Entity
ID id.ID `json:"id"`
FlagKey string `json:"flag_key"`
AppID string `json:"app_id"`
TenantID string `json:"tenant_id"`
Value any `json:"value"`
}Engine
The Engine evaluates flags using definitions, rules, and overrides from the store.
Creating an engine
engine := flag.NewEngine(store,
flag.WithCacheTTL(30*time.Second),
)| EngineOption | Description |
|---|---|
WithCacheTTL(ttl time.Duration) | Enables an in-memory TTL cache for evaluation results |
Evaluate
func (e *Engine) Evaluate(ctx context.Context, key, appID string) (any, error)Evaluation order
The engine follows a strict evaluation order:
- Disabled check -- If
Definition.Enabledisfalse, returnDefaultValueimmediately. - Tenant override -- If a tenant ID is in the context and a
TenantOverrideexists for that tenant, return the override value. - Rules -- Evaluate targeting rules in priority order (ascending). Return the
ReturnValueof the first matching rule. - Default -- If no rule matches, return
Definition.DefaultValue.
Context keys
The engine reads tenant and user identifiers from the context:
const (
ContextKeyTenantID ContextKey = "vault.tenant_id"
ContextKeyUserID ContextKey = "vault.user_id"
)Set these using the scope package helpers:
import "github.com/xraph/vault/scope"
ctx = scope.WithTenantID(ctx, "tenant-abc")
ctx = scope.WithUserID(ctx, "user-123")Rollout hashing
The rollout rule type uses deterministic hashing to ensure consistent assignment:
hash = SHA-256(tenantID + ":" + flagKey)
bucket = first 4 bytes of hash (big-endian uint32) mod 100
match = bucket < percentageThis means the same tenant always gets the same result for a given flag, and changing the percentage only adds new tenants (never removes existing ones below the threshold).
Service
The Service wraps the engine with type-safe accessors. On error or type mismatch, each accessor returns its defaultVal argument.
Creating a service
svc := flag.NewService(engine,
flag.WithAppID("myapp"),
)Type-safe accessors
Bool
func (s *Service) Bool(ctx context.Context, key string, defaultVal bool) boolif svc.Bool(ctx, "dark-mode", false) {
enableDarkMode()
}String
func (s *Service) String(ctx context.Context, key, defaultVal string) stringvariant := svc.String(ctx, "checkout-flow", "standard")Int
func (s *Service) Int(ctx context.Context, key string, defaultVal int) intHandles int, float64 (from JSON), and int64 values.
maxItems := svc.Int(ctx, "cart-max-items", 100)Float
func (s *Service) Float(ctx context.Context, key string, defaultVal float64) float64Handles float64, int, and int64 values (promoted to float64).
threshold := svc.Float(ctx, "score-threshold", 0.75)JSON
func (s *Service) JSON(ctx context.Context, key string, target any) errorEvaluates the flag and unmarshals the value into target (must be a pointer). Supports []byte, string, and arbitrary values via marshal/unmarshal round-trip.
type LayoutConfig struct {
Columns int `json:"columns"`
Theme string `json:"theme"`
}
var cfg LayoutConfig
if err := svc.JSON(ctx, "layout-config", &cfg); err != nil {
cfg = LayoutConfig{Columns: 2, Theme: "default"}
}Evaluation cache
When WithCacheTTL is set on the engine, evaluation results are cached in memory keyed by flagKey + tenantID. The cache:
- Uses a TTL-based expiration strategy.
- Is thread-safe (protected by
sync.RWMutex). - Supports invalidation by flag key (
invalidate) or full flush (invalidateAll). - Reduces store reads for hot flags.
Store interface
type Store interface {
DefineFlag(ctx context.Context, f *Definition) error
GetFlagDefinition(ctx context.Context, key, appID string) (*Definition, error)
ListFlagDefinitions(ctx context.Context, appID string, opts ListOpts) ([]*Definition, error)
DeleteFlagDefinition(ctx context.Context, key, appID string) error
SetFlagRules(ctx context.Context, key, appID string, rules []*Rule) error
GetFlagRules(ctx context.Context, key, appID string) ([]*Rule, error)
SetFlagTenantOverride(ctx context.Context, key, appID, tenantID string, value any) error
GetFlagTenantOverride(ctx context.Context, key, appID, tenantID string) (any, error)
DeleteFlagTenantOverride(ctx context.Context, key, appID, tenantID string) error
ListFlagTenantOverrides(ctx context.Context, key, appID string) ([]*TenantOverride, error)
}Full example
package main
import (
"context"
"fmt"
"time"
"github.com/xraph/vault/flag"
"github.com/xraph/vault/scope"
)
func main() {
ctx := context.Background()
// Create engine with caching.
engine := flag.NewEngine(store,
flag.WithCacheTTL(30*time.Second),
)
// Create type-safe service.
svc := flag.NewService(engine,
flag.WithAppID("myapp"),
)
// Define a flag (via store).
_ = store.DefineFlag(ctx, &flag.Definition{
Key: "new-checkout",
Type: flag.TypeBool,
DefaultValue: false,
Enabled: true,
AppID: "myapp",
Description: "Enables the new checkout flow",
})
// Add targeting rules.
_ = store.SetFlagRules(ctx, "new-checkout", "myapp", []*flag.Rule{
flag.WhenTenant("tenant-beta").Return(true),
flag.Rollout(10).Return(true),
})
// Set a per-tenant override.
_ = store.SetFlagTenantOverride(ctx, "new-checkout", "myapp", "tenant-vip", true)
// Evaluate with tenant context.
ctx = scope.WithTenantID(ctx, "tenant-beta")
if svc.Bool(ctx, "new-checkout", false) {
fmt.Println("New checkout enabled!")
}
}