Skip to content

bordviz/featureflags

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

featureflags

Go Reference Go Version License

Lightweight, thread-safe feature flags library for Go applications with support for gradual rollouts, allowlists, and denylists.


Features

  • Dynamic Configuration - Hot-reload flags from HTTP endpoints or local files
  • Gradual Rollouts - Enable features for a percentage of users using deterministic hashing
  • User Targeting - Allowlist/Denylist specific users regardless of rollout percentage
  • Thread-Safe - Safe for concurrent use across thousands of goroutines
  • Minimal Dependencies - Core uses only Go standard library (log/slog, sync, hash/crc32)
  • Extensible - Implement the Provider interface to fetch configs from any source

Content


Installation

go get -u github.com/bordviz/featureflags

Requirements: Go 1.21+ (uses log/slog)


Quick Start

package main

import (
    "time"
    "log/slog"
    
    "github.com/bordviz/featureflags"
    "github.com/bordviz/featureflags/provider"
)

func main() {
    // Create a provider (HTTP or File)
    prov := provider.NewHTTPProvider("https://config.example.com/flags.json")
    
    // Initialize the client
    client := featureflags.NewClient(prov,
        featureflags.WithInterval(30*time.Second),
        featureflags.WithLogger(slog.Default()),
    )
    defer client.Close() // Always close to stop the background updater
    
    // Check if a feature is enabled for a user
    if client.IsEnabled("new-checkout-flow", "user_12345") {
        renderNewCheckout()
    } else {
        renderLegacyCheckout()
    }
}

Configuration Format

The library supports JSON and YAML configuration files (for FileProvider). HTTPProvider expects JSON.

JSON Example

{
  "version": "1.0.0",
  "flags": {
    "new-checkout-flow": {
      "key": "new-checkout-flow",
      "enabled": true,
      "rule": {
        "rollout_percentage": 25,
        "allowlist": ["user_admin", "user_beta_001"],
        "denylist": ["user_blocked_999"]
      }
    },
    "dark-mode": {
      "key": "dark-mode",
      "enabled": false,
      "rule": {
        "rollout_percentage": 0,
        "allowlist": [],
        "denylist": []
      }
    }
  }
}

YAML Example

version: "1.0.0"
flags:
  new-checkout-flow:
    key: new-checkout-flow
    enabled: true
    rule:
      rollout_percentage: 25
      allowlist:
        - user_admin
        - user_beta_001
      denylist:
        - user_blocked_999
  dark-mode:
    key: dark-mode
    enabled: false
    rule:
      rollout_percentage: 0
      allowlist: []
      denylist: []

Examples

Examples

The examples/ directory contains working demonstrations of different use cases.


API Reference

Client

The Client is the main entry point for evaluating feature flags. It manages a background goroutine that periodically fetches updated configurations from the provider.

NewClient

func NewClient(provider Provider, opts ...ClientOption) *Client

Creates a new feature flags client. Starts a background updater goroutine immediately.

Default Options:

  • Interval: 1 minute
  • Logger: slog.New(slog.DiscardHandler)

Client Options:

Option Description Default
WithInterval(time.Duration) Config refresh interval 1 minute
WithLogger(*slog.Logger) Logger for background errors DiscardHandler

IsEnabled

func (c *Client) IsEnabled(flagKey, userID string) bool

Returns true if the flag is enabled for the specified user. This is an O(1) operation safe for high-traffic use.

GetConfig

func (c *Client) GetConfig() Config

Returns a copy of the current configuration. Useful for debugging or bulk flag checks.

Close

func (c *Client) Close()

Stops the background configuration updater goroutine. Must be called when shutting down your application to prevent goroutine leaks.


Provider Interface

type Provider interface {
    Fetch(ctx context.Context) (*Config, error)
}

Implement this interface to load configurations from any source. The provider is called periodically by the client's background updater.


Built-in Providers

HTTPProvider

Fetches JSON configuration from a remote HTTP endpoint.

// Basic usage
prov := provider.NewHTTPProvider("https://api.example.com/flags")

// With custom options
prov := provider.NewHTTPProvider(
    "https://api.example.com/flags",
    provider.WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
    provider.WithHeader("Authorization", "Bearer token123"),
    provider.WithPreprocess(func(data []byte) ([]byte, error) {
        // Decrypt, decompress, or transform data before parsing
        return data, nil
    }),
)

Default Settings:

  • HTTP Client Timeout: 10 seconds
  • Default Header: Accept: application/json

Options:

Option Description
WithHTTPClient(*http.Client) Set custom HTTP client
WithHeader(key, value string) Add custom HTTP header
WithPreprocess(func([]byte) ([]byte, error)) Transform response before parsing

FileProvider

Loads configuration from a local file. Supports JSON and YAML formats.

// JSON file (default)
prov := provider.NewFileProvider("./flags.json")

// YAML file
prov := provider.NewFileProvider(
    "./flags.yaml",
    provider.WithFormat(provider.FileFormatYAML),
)

Options:

Option Description Default
WithFormat(FileFormat) Set file format (JSON or YAML) JSON

Data Structures

Config

type Config struct {
    Flags   map[string]Flag `json:"flags" yaml:"flags"`
    Version string          `json:"version" yaml:"version"`
}

Main configuration structure containing all feature flags.

Flag

type Flag struct {
    Key     string   `json:"key" yaml:"key"`
    Enabled bool     `json:"enabled" yaml:"enabled"`
    Rule    FlagRule `json:"rule" yaml:"rule"`
}

Represents a single feature flag with its evaluation rules.

FlagRule

type FlagRule struct {
    RolloutPercentage int      `json:"rollout_percentage" yaml:"rollout_percentage"`
    Allowlist         []string `json:"allowlist" yaml:"allowlist"`
    Denylist          []string `json:"denylist" yaml:"denylist"`
}

Defines conditions for enabling a flag for a specific user.

Field Description Range
RolloutPercentage Percentage of users for whom the flag is enabled 0-100
Allowlist User IDs for whom the flag is always enabled -
Denylist User IDs for whom the flag is always disabled -

Evaluation Logic

When IsEnabled(flagKey, userID) is called, the flag is evaluated in the following order:

  1. Flag Exists - If the flag key does not exist in the configuration, return false
  2. Global Enabled - If flag.Enabled is false, return false
  3. Allowlist Check - If userID is in the Allowlist, return true
  4. Denylist Check - If userID is in the Denylist, return false
  5. Rollout Percentage - If hash(flagKey:userID) % 100 < RolloutPercentage, return true
  6. Default - Return false

Note: Allowlist takes precedence over Denylist. If a user is in both lists, Allowlist wins.


Rollout Percentage Implementation

The library uses deterministic hashing to ensure consistent user experience across application restarts and multiple instances.

// Internal implementation
input := fmt.Sprintf("%s:%s", flagKey, userID)
hash := crc32.ChecksumIEEE([]byte(input))
bucket := int(hash % 100)
enabled := bucket < rolloutPercentage

Properties

  • Consistency - Same user + same flag = same result across restarts and instances
  • No External State - Works offline, does not require Redis or database
  • Even Distribution - CRC32 ensures uniform distribution across buckets 0-99
  • High Performance - O(1) operation, no network calls during evaluation

Example

// User user_123 will always be in the same bucket for flag new-feature
bucket1 := hash.GetUserBucket("new-feature", "user_123") // e.g., 42
bucket2 := hash.GetUserBucket("new-feature", "user_123") // always 42

// Different flags give different buckets for the same user
bucket3 := hash.GetUserBucket("other-feature", "user_123") // e.g., 17

Error Handling

Provider Errors

If the provider returns an error during configuration fetch:

  • The error is logged via the configured slog.Logger
  • The client continues working with the last successful configuration
  • New flag updates are not available until the next successful fetch

Missing Flags

If a flag key does not exist in the configuration:

  • IsEnabled returns false
  • No error is returned

Nil Configuration

If the initial fetch fails and no configuration is loaded:

  • All IsEnabled calls return false
  • The background updater continues retrying at the configured interval

Graceful Shutdown

Always close the client to stop the background configuration updater goroutine.

client := featureflags.NewClient(provider)
defer client.Close()

Example with Signal Handling

package main

import (
    "context"
    "os"
    "os/signal"
    "syscall"
    "time"
    
    "github.com/bordviz/featureflags"
    "github.com/bordviz/featureflags/provider"
)

func main() {
    prov := provider.NewHTTPProvider("https://config.example.com/flags.json")
    client := featureflags.NewClient(prov)
    
    // Wait for shutdown signal
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
    <-sigCh
    
    // Gracefully close the client
    client.Close()
    
    // Allow time for cleanup
    time.Sleep(100 * time.Millisecond)
}

Thread Safety

The library is designed for concurrent use:

  • Storage - Uses sync.RWMutex for thread-safe read/write operations
  • IsEnabled - Safe to call from thousands of goroutines simultaneously
  • Background Updater - Runs in a separate goroutine, does not block flag evaluation

Race Condition Protection

  • GetConfig returns a copy of the configuration to prevent race conditions
  • Storage uses RLock for reads and Lock for writes
  • All public methods are safe for concurrent use

Custom Provider Example

Implement the Provider interface to load configurations from any source.

type EtcdProvider struct {
    client *etcd.Client
    key    string
}

func (p *EtcdProvider) Fetch(ctx context.Context) (*featureflags.Config, error) {
    resp, err := p.client.Get(ctx, p.key)
    if err != nil {
        return nil, err
    }
    
    var cfg featureflags.Config
    if err := json.Unmarshal(resp.Kvs[0].Value, &cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}

// Usage
client := featureflags.NewClient(&EtcdProvider{client: etcdClient, key: "/flags"})

License

Distributed under the MIT License. See LICENSE for more information.

Made with ❤️ for the Go community

About

Lightweight, thread-safe feature flags library for Go applications with support for gradual rollouts, allowlists, and denylists.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages