CrackedRuby CrackedRuby

Overview

Feature flags, also known as feature toggles or feature switches, provide a technique for modifying system behavior without changing code. A feature flag wraps conditional logic around code paths, allowing features to be enabled or disabled through configuration changes rather than deployments. This decouples deployment from release, giving teams control over when users see new functionality.

The concept emerged from continuous delivery practices where teams needed to deploy code to production frequently while maintaining control over feature visibility. Rather than maintaining long-lived feature branches, developers merge incomplete features to the main branch behind disabled flags. Operations teams can then enable features selectively for specific users, environments, or timeframes.

Feature flags address several software delivery challenges. They reduce deployment risk by allowing immediate rollback without redeployment. They enable testing in production with real traffic. They support gradual rollouts to catch issues before full release. They facilitate A/B testing and experimentation. They provide emergency kill switches for problematic features.

The basic implementation involves a decision point in code that queries flag state:

if FeatureFlags.enabled?(:new_checkout_flow)
  render_new_checkout
else
  render_old_checkout
end

This simple conditional creates significant operational flexibility. The flag state can be determined by configuration files, database records, environment variables, or remote services. The decision can incorporate user attributes, percentage rollouts, time windows, or complex rules.

Feature flags operate at different layers within an application architecture. UI flags control visual elements and user interactions. Backend flags modify business logic and data processing. Infrastructure flags adjust system behavior like caching strategies or connection pooling. Each layer requires different consideration for consistency, performance, and testing.

Key Principles

Feature flags operate on a decision model where code execution paths branch based on flag evaluation. The evaluation occurs at runtime, not compile time or deployment time. This runtime decision-making requires infrastructure to manage flag state and evaluate conditions efficiently.

The flag evaluation process follows a consistent pattern. First, the system identifies the evaluation context—typically the current user, request, or operation. Second, it retrieves flag configuration from a flag source. Third, it applies evaluation rules to determine the flag state for this context. Fourth, it returns a boolean or variant value that directs code execution. This process must complete quickly since it occurs on the request path.

Flag state exists independently from application code. While code contains decision points that check flag values, the actual state lives in external configuration. This separation allows flag changes without code changes. The configuration source can be local files for simple cases, databases for dynamic updates, or dedicated flag management services for sophisticated scenarios.

Evaluation context determines which flag variation applies. Static flags return the same value for all evaluations. User-based flags vary by user identifier, allowing targeted enablement. Percentage-based flags enable features for a proportion of traffic. Multi-variate flags return multiple possible values for A/B testing scenarios. The evaluation logic combines context attributes with flag rules to produce the result.

Flag lifecycle management represents a critical principle. Flags pass through distinct stages: creation during development, activation in production, monitoring during rollout, and removal after full release. Teams must treat flags as temporary scaffolding rather than permanent configuration. Flags that remain in code indefinitely create technical debt and increase system complexity.

Flag types serve different purposes and require different management approaches. Release flags control rollout of complete features. Experiment flags enable A/B tests and multivariate experiments. Operational flags provide kill switches for performance or load management. Permission flags implement authorization logic. Each type has different lifetime expectations and removal criteria.

The decision architecture must handle evaluation failures gracefully. When flag evaluation fails due to timeout, service unavailability, or configuration errors, the system needs a defined fallback behavior. Defaulting to enabled creates risk if the feature has issues. Defaulting to disabled prevents users from accessing completed features. The choice depends on feature characteristics and risk tolerance.

Consistency requirements vary by flag purpose. User-targeted flags must return consistent results for the same user across requests to prevent confusion. Percentage-based flags can tolerate some inconsistency during short timeframes. Experiment flags require strict consistency to maintain statistical validity. The flag infrastructure must provide appropriate consistency guarantees for each use case.

Design Considerations

Feature flag strategies present trade-offs between simplicity and capability. Simple boolean flags with static configuration minimize infrastructure requirements but limit flexibility. Dynamic flags with percentage rollouts require more infrastructure but enable gradual releases. Multi-variate flags supporting complex targeting rules demand sophisticated evaluation engines but provide precise control. Teams must balance infrastructure investment against operational needs.

The decision to introduce a feature flag carries implications for code complexity and maintenance burden. Each flag creates an additional code path that requires testing and maintenance. The codebase complexity grows exponentially with nested flags—two flags create four possible states, three create eight. Teams should evaluate whether the operational benefits justify the added complexity for each flag.

Flag granularity affects both flexibility and complexity. Coarse-grained flags controlling entire features reduce the number of decision points but limit control precision. Fine-grained flags wrapping individual components increase flexibility but multiply decision points and testing scenarios. The granularity choice should reflect the actual deployment and rollback needs for the feature.

Long-lived versus short-lived flags require different architectural approaches. Short-lived release flags can tolerate simpler infrastructure since removal is imminent. Long-lived operational or permission flags justify investment in robust management tools. Mixing both types in the same system requires distinguishing them clearly and enforcing removal processes for temporary flags.

Flag evaluation location impacts performance and consistency. Client-side evaluation in JavaScript or mobile apps reduces server load but complicates updates and creates inconsistency windows. Server-side evaluation centralizes control and ensures consistency but adds latency to requests. Edge evaluation at CDN or API gateway level balances performance and consistency. The location choice depends on feature requirements and infrastructure capabilities.

Default flag states determine system behavior during failures or missing configuration. Defaulting flags to disabled provides safety but may hide completed features if configuration fails. Defaulting to enabled reduces operational friction but increases risk for problematic features. Some systems use different defaults for different flag types—release flags default disabled for safety while permission flags default enabled for availability.

Configuration storage affects flag management capabilities. File-based configuration provides simplicity and version control integration but requires deployments for changes. Database storage enables dynamic updates but introduces database dependencies. Remote flag services offer sophisticated targeting and gradual rollout but create external dependencies. Environment variables work for simple cases but scale poorly.

Testing strategies must account for all flag combinations. Testing each path independently ensures correctness but creates exponential test cases as flags multiply. Testing only the enabled and disabled states for each flag reduces coverage but remains practical. Parameterized tests that exercise flags systematically help manage this complexity. Teams need policies about which combinations require explicit testing.

Flag removal requires coordinated steps across code, configuration, and monitoring. Code removal must wait until flags reach 100% enablement in all environments. Configuration cleanup prevents ghost flags from cluttering systems. Monitoring ensures no code still references removed flags. The removal process needs clear ownership and scheduling to prevent flags from accumulating indefinitely.

Ruby Implementation

Ruby applications implement feature flags through conditional logic that queries flag state. The simplest implementation uses environment variables or configuration files to store flag state and checks these values at runtime.

class FeatureFlags
  def self.enabled?(flag_name)
    flags = YAML.load_file('config/features.yml')
    flags[flag_name.to_s] || false
  end
end

# Usage in controllers or models
if FeatureFlags.enabled?(:new_dashboard)
  @dashboard = NewDashboard.new(user)
else
  @dashboard = LegacyDashboard.new(user)
end

This basic pattern separates flag state from code but reloads configuration on every check. Production systems need caching to avoid file system overhead.

The Flipper gem provides a comprehensive Ruby feature flag framework. Flipper supports multiple backends including memory, Redis, and ActiveRecord. It offers sophisticated targeting including user-based, group-based, percentage-of-actors, and percentage-of-time enablement.

# Gemfile
gem 'flipper'
gem 'flipper-active_record'

# config/initializers/flipper.rb
require 'flipper/adapters/active_record'

Flipper.configure do |config|
  config.adapter { Flipper::Adapters::ActiveRecord.new }
end

# Create a feature
Flipper.enable(:new_search)

# Enable for specific user
user = User.find(123)
Flipper.enable_actor(:premium_features, user)

# Enable for percentage of users
Flipper.enable_percentage_of_actors(:beta_ui, 25)

# Enable for group
Flipper.register(:admins) do |actor|
  actor.respond_to?(:admin?) && actor.admin?
end
Flipper.enable_group(:dangerous_features, :admins)

# Check in application code
if Flipper.enabled?(:new_search, current_user)
  search_results = NewSearchEngine.search(query)
else
  search_results = LegacySearch.search(query)
end

Flipper's actor-based targeting requires objects to respond to flipper_id. ActiveRecord models automatically implement this using the primary key. Custom objects need to define this method.

class GuestUser
  def flipper_id
    "Guest:#{session_id}"
  end
end

guest = GuestUser.new(session_id: 'abc123')
Flipper.enabled?(:guest_checkout, guest)

The Rollout gem offers Redis-backed feature flags with simple percentage and user-based targeting. It requires less setup than Flipper but provides fewer targeting options.

# Gemfile
gem 'rollout'

# Initialize with Redis
$redis = Redis.new
$rollout = Rollout.new($redis)

# Activate for percentage
$rollout.activate_percentage(:new_feature, 20)

# Activate for specific users
$rollout.activate_user(:new_feature, User.find(42))

# Activate for groups
$rollout.define_group(:beta_users) do |user|
  user.beta_tester?
end
$rollout.activate_group(:new_feature, :beta_users)

# Check flag
if $rollout.active?(:new_feature, current_user)
  # New code path
end

Rails applications often integrate flags at the controller or view layer. Flags can control entire endpoints, specific view sections, or background job behavior.

class DashboardController < ApplicationController
  def show
    if Flipper.enabled?(:react_dashboard, current_user)
      render :show_react
    else
      render :show_legacy
    end
  end
end

# In views
<% if Flipper.enabled?(:new_navigation, current_user) %>
  <%= render 'shared/new_nav' %>
<% else %>
  <%= render 'shared/legacy_nav' %>
<% end %>

# In background jobs
class ReportGenerator
  def perform(user_id)
    user = User.find(user_id)
    if Flipper.enabled?(:parallel_processing, user)
      generate_parallel(user)
    else
      generate_sequential(user)
    end
  end
end

Custom flag implementations can wrap business logic for specific evaluation needs. This pattern centralizes flag checking and provides type-safe flag names.

module FeatureGates
  def self.new_pricing_enabled?(user)
    return true if user.admin?
    return true if user.early_adopter?
    Flipper.enabled?(:new_pricing, user)
  end

  def self.experimental_algorithm?(context)
    return false unless context.production?
    Flipper.enabled_percentage_of_time?(:experimental_algorithm, 5)
  end
end

# Usage
if FeatureGates.new_pricing_enabled?(current_user)
  price = NewPricingEngine.calculate(order)
else
  price = LegacyPricing.calculate(order)
end

Flag state can be exposed through APIs for debugging and monitoring. Rails applications can create endpoints that report current flag configurations.

class Admin::FeatureFlagsController < ApplicationController
  before_action :require_admin

  def index
    @flags = Flipper.features.map do |feature|
      {
        name: feature.key,
        state: feature.state,
        enabled_gates: feature.enabled_gates.map(&:name)
      }
    end
  end

  def update
    feature = Flipper[params[:name]]
    if params[:enabled]
      feature.enable
    else
      feature.disable
    end
    redirect_to admin_feature_flags_path
  end
end

Implementation Approaches

Static configuration flags use files or environment variables to store flag state. This approach requires deployment or process restart to change flags but provides simplicity and version control integration. Configuration files can be YAML, JSON, or Ruby code.

# config/features.yml
new_checkout: true
beta_dashboard: false
experimental_search: true

# Load and cache at boot
FEATURES = YAML.load_file('config/features.yml').freeze

def feature_enabled?(name)
  FEATURES[name.to_s] || false
end

Environment-based flags store state in environment variables, useful for container deployments where environment configuration changes trigger restarts.

def feature_enabled?(name)
  ENV["FEATURE_#{name.to_s.upcase}"] == 'true'
end

# Set via environment
# FEATURE_NEW_CHECKOUT=true rails server

Database-backed flags store state in database tables, allowing dynamic updates without deployment. This approach requires database queries on flag checks unless combined with caching.

class Feature < ApplicationRecord
  def self.enabled?(name)
    Rails.cache.fetch("feature:#{name}", expires_in: 1.minute) do
      find_by(name: name)&.enabled || false
    end
  end
end

# Toggle via ActiveRecord
Feature.find_or_create_by(name: 'new_api').update(enabled: true)

Remote flag services use external APIs to fetch flag configuration. Services like LaunchDarkly, Split, or ConfigCat provide sophisticated targeting and real-time updates. This introduces network dependencies but offers powerful management interfaces.

# Using LaunchDarkly SDK
require 'ldclient-rb'

client = LaunchDarkly::LDClient.new(ENV['LAUNCHDARKLY_SDK_KEY'])

user = {
  key: current_user.id.to_s,
  email: current_user.email,
  custom: {
    plan: current_user.subscription_plan
  }
}

if client.variation('new-feature', user, false)
  # Feature enabled
end

Hybrid approaches combine multiple strategies for different flag types. Short-lived release flags might use database storage for easy toggling while long-lived operational flags use environment variables for stability.

class FeatureManager
  def self.enabled?(flag_name, user = nil)
    # Check release flags in database
    if ReleaseFlag.exists?(flag_name)
      return ReleaseFlag.enabled?(flag_name, user)
    end
    
    # Check operational flags in environment
    if ENV["FEATURE_#{flag_name.to_s.upcase}"]
      return ENV["FEATURE_#{flag_name.to_s.upcase}"] == 'true'
    end
    
    # Default to disabled
    false
  end
end

Percentage-based rollout requires deterministic hash functions to ensure consistency. Users with the same identifier always receive the same result.

def enabled_for_percentage?(flag_name, user_id, percentage)
  hash = Digest::MD5.hexdigest("#{flag_name}:#{user_id}")[0..7].to_i(16)
  (hash % 100) < percentage
end

# 25% of users see new feature
if enabled_for_percentage?(:new_ui, current_user.id, 25)
  render_new_ui
else
  render_old_ui
end

Circuit breaker flags provide automatic disablement when error rates exceed thresholds. This requires monitoring infrastructure to track errors and update flag state.

class CircuitBreakerFlag
  def self.enabled?(flag_name)
    error_rate = ErrorTracker.rate_for(flag_name, window: 5.minutes)
    threshold = Flag.find(flag_name).error_threshold
    
    if error_rate > threshold
      Flag.find(flag_name).update(enabled: false)
      AlertService.notify("Circuit breaker opened for #{flag_name}")
      false
    else
      Flag.find(flag_name).enabled?
    end
  end
end

Common Patterns

Canary releases use percentage-based flags to gradually expose features to increasing user populations. Start with 1-5% of users, monitor metrics, and increase if healthy.

# Initial rollout
Flipper.enable_percentage_of_actors(:new_algorithm, 1)

# Monitor metrics, then increase
Flipper.enable_percentage_of_actors(:new_algorithm, 5)
Flipper.enable_percentage_of_actors(:new_algorithm, 25)
Flipper.enable_percentage_of_actors(:new_algorithm, 50)
Flipper.enable_percentage_of_actors(:new_algorithm, 100)

# Implementation checks the flag
def process_data(data, user)
  if Flipper.enabled?(:new_algorithm, user)
    NewAlgorithm.process(data)
  else
    LegacyAlgorithm.process(data)
  end
end

Kill switches provide immediate disablement for problematic features. These flags default to enabled and can be flipped to disabled during incidents.

# Normal state: enabled
Flipper.enable(:background_processing)

# In emergency, disable immediately
Flipper.disable(:background_processing)

# Code checks flag before expensive operation
class DataProcessor
  def process
    unless Flipper.enabled?(:background_processing)
      logger.warn("Background processing disabled via kill switch")
      return
    end
    
    # Expensive operation
    perform_processing
  end
end

A/B testing flags return multiple variants to split traffic between different implementations. This requires tracking which variant users see for analytics.

# Define variants
Flipper.register(:variant_selector) do |actor, feature|
  hash = Digest::MD5.hexdigest("#{actor.flipper_id}:#{feature.key}")[0..7].to_i(16)
  case hash % 3
  when 0 then :control
  when 1 then :variant_a
  when 2 then :variant_b
  end
end

# Use variant in code
def show_pricing
  variant = determine_variant(:pricing_experiment, current_user)
  
  Analytics.track('pricing_shown', {
    user_id: current_user.id,
    variant: variant
  })
  
  case variant
  when :control
    render_original_pricing
  when :variant_a
    render_pricing_variant_a
  when :variant_b
    render_pricing_variant_b
  end
end

Migration patterns wrap database or infrastructure changes behind flags, allowing rollback without data loss.

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :email
      # Add new column behind flag
      t.string :encrypted_email if Feature.enabled?(:encrypted_emails)
      t.timestamps
    end
  end
end

class User < ApplicationRecord
  def email
    if Flipper.enabled?(:encrypted_emails, self)
      decrypt(encrypted_email)
    else
      read_attribute(:email)
    end
  end
  
  def email=(value)
    if Flipper.enabled?(:encrypted_emails, self)
      self.encrypted_email = encrypt(value)
    else
      write_attribute(:email, value)
    end
  end
end

Graceful degradation flags disable non-critical features during high load or partial outages.

class RecommendationEngine
  def recommendations_for(user)
    # Expensive ML-based recommendations
    if Flipper.enabled?(:ml_recommendations) && !HighLoad.current?
      ml_recommendations(user)
    else
      # Fallback to simple recommendations
      popular_items.sample(10)
    end
  end
end

class HighLoad
  def self.current?
    # Check system metrics
    SystemMetrics.cpu_usage > 80 || SystemMetrics.queue_depth > 1000
  end
end

Beta program flags enable features for opted-in users or specific customer segments.

Flipper.register(:beta_users) do |actor|
  actor.respond_to?(:beta_participant?) && actor.beta_participant?
end

Flipper.enable_group(:new_api, :beta_users)

# Users join beta program
class User < ApplicationRecord
  def join_beta!
    update(beta_participant: true)
  end
end

# API endpoint checks flag
class Api::V2::ResourcesController < ApplicationController
  before_action :check_api_access
  
  def check_api_access
    unless Flipper.enabled?(:new_api, current_user)
      render json: { error: 'API access not enabled' }, status: 403
    end
  end
end

Time-based flags enable features during specific windows, useful for scheduled releases or temporary promotions.

class ScheduledFeature
  def self.enabled?(flag_name)
    config = Flag.find_by(name: flag_name)
    return false unless config
    
    now = Time.current
    return false if config.start_time && now < config.start_time
    return false if config.end_time && now > config.end_time
    
    config.enabled?
  end
end

# Holiday promotion
Flag.create(
  name: 'holiday_banner',
  enabled: true,
  start_time: '2025-12-01 00:00:00',
  end_time: '2025-12-31 23:59:59'
)

Tools & Ecosystem

Flipper remains the most popular Ruby feature flag library. It provides adapters for multiple storage backends including memory, Redis, ActiveRecord, and Sequel. The flipper-ui gem adds a web interface for flag management.

# Gemfile
gem 'flipper'
gem 'flipper-active_record'
gem 'flipper-ui'
gem 'flipper-redis'

# config/routes.rb
mount Flipper::UI.app(Flipper) => '/admin/flipper'

# Different adapters
Flipper.configure do |config|
  # ActiveRecord for persistence
  config.adapter { Flipper::Adapters::ActiveRecord.new }
  
  # Or Redis for performance
  # config.adapter { Flipper::Adapters::Redis.new(Redis.new) }
  
  # Or memory for testing
  # config.adapter { Flipper::Adapters::Memory.new }
end

LaunchDarkly provides an enterprise feature flag service with SDKs for Ruby and other languages. It offers percentage rollouts, user targeting, and real-time flag updates without deployment.

gem 'launchdarkly-server-sdk'

client = LaunchDarkly::LDClient.new(ENV['LAUNCHDARKLY_SDK_KEY'])

user = LaunchDarkly::Context.create({
  key: current_user.id.to_s,
  kind: 'user',
  email: current_user.email,
  plan: current_user.plan
})

show_feature = client.variation('new-checkout', user, false)

Split offers feature flags with built-in experimentation and analytics. The Ruby SDK supports sophisticated targeting rules and integrates with Split's metrics platform.

gem 'splitclient-rb'

factory = SplitIoClient::SplitFactoryBuilder.build(ENV['SPLIT_API_KEY'])
client = factory.client

treatment = client.get_treatment(user.id, 'new_recommendation_engine')

case treatment
when 'on'
  new_recommendations
when 'off'
  old_recommendations
else
  default_recommendations
end

Unleash provides open-source feature flag management with self-hosting options. The Ruby SDK connects to Unleash servers for flag configuration.

gem 'unleash'

Unleash.configure do |config|
  config.app_name = 'my-rails-app'
  config.url = ENV['UNLEASH_URL']
  config.custom_http_headers = { 'Authorization': ENV['UNLEASH_API_KEY'] }
end

if Unleash.is_enabled?('new_feature', unleash_context)
  # Feature code
end

Rollout provides Redis-backed flags with simple syntax. It works well for applications already using Redis for caching or sessions.

gem 'rollout'

$rollout = Rollout.new($redis)
$rollout.activate(:new_feature)
$rollout.active?(:new_feature, current_user)

Feature provides a lightweight alternative without external dependencies. Flags are defined in Ruby code or configuration files.

gem 'feature'

Feature.set(:new_ui, true)
Feature.active?(:new_ui)

# Per-user features
Feature.active?(:premium_features, current_user)

Environment-based flag tools like Figaro or dotenv manage flags through environment variables. This works well for simple on/off flags across environments.

gem 'figaro'

# config/application.yml
FEATURE_NEW_DASHBOARD: true
FEATURE_BETA_API: false

# Use in code
if ENV['FEATURE_NEW_DASHBOARD'] == 'true'
  # Feature code
end

Common Pitfalls

Flag accumulation occurs when teams create flags but never remove them. Each unreleased flag adds code complexity and test scenarios. Systems can accumulate hundreds of dead flags over time.

# Technical debt from old flags
if FeatureFlags.enabled?(:new_search) # Added 2 years ago, always true
  if FeatureFlags.enabled?(:search_filters) # Added 1 year ago, always true
    if FeatureFlags.enabled?(:faceted_search) # Current flag
      # Actual code path used
    end
  end
end

# Solution: Remove flags after full rollout
# Just the current logic remains
if FeatureFlags.enabled?(:faceted_search)
  # Clean code path
end

Inconsistent flag state across servers causes users to see different behavior on subsequent requests. This happens when flags use local caching without invalidation or when database-backed flags lack proper cache coordination.

# Problem: Each server has its own cache
class Feature
  def self.enabled?(name)
    @cache ||= {}
    @cache[name] ||= Feature.find_by(name: name)&.enabled || false
  end
end

# Solution: Use shared cache with TTL
class Feature
  def self.enabled?(name)
    Rails.cache.fetch("feature:#{name}", expires_in: 30.seconds) do
      Feature.find_by(name: name)&.enabled || false
    end
  end
end

Testing complexity multiplies with nested flags. Two flags create four code paths, three create eight, four create sixteen. Testing all combinations becomes impractical.

# Problem: Exponential test cases
def checkout_flow
  if flags.enabled?(:new_checkout)
    if flags.enabled?(:guest_checkout)
      if flags.enabled?(:one_click)
        # Path 1
      else
        # Path 2
      end
    else
      # Path 3
    end
  else
    # Path 4
  end
end

# Solution: Reduce nesting, use feature-complete flags
def checkout_flow
  return one_click_checkout if flags.enabled?(:checkout_v2)
  legacy_checkout
end

Default values for missing flags can hide configuration errors. Applications may run with all flags disabled or enabled due to missing configuration, masking deployment problems.

# Problem: Silent failure hides missing config
def feature_enabled?(name)
  FEATURES[name] || false # Always false if config file missing
end

# Solution: Fail fast on missing critical flags
def feature_enabled?(name)
  unless FEATURES.key?(name)
    raise "Unknown feature flag: #{name}"
  end
  FEATURES[name]
end

Flag evaluation in tight loops creates performance bottlenecks. Checking flags on every iteration of processing thousands of records adds significant overhead.

# Problem: Flag check in loop
users.each do |user|
  if FeatureFlags.enabled?(:new_processing, user) # Checks flag 10,000 times
    new_process(user)
  end
end

# Solution: Check once before loop or batch
enabled_users = users.select { |u| FeatureFlags.enabled?(:new_processing, u) }
enabled_users.each { |user| new_process(user) }

Percentage rollout can split user sessions if flag checks happen at different times without sticky sessions. Users may see feature A on one request and feature B on the next.

# Problem: Non-deterministic flag evaluation
def enabled?(flag, user)
  rand(100) < Flag.find(flag).percentage # Different each time
end

# Solution: Deterministic hash-based evaluation
def enabled?(flag, user)
  hash = Digest::MD5.hexdigest("#{flag}:#{user.id}")[0..7].to_i(16)
  (hash % 100) < Flag.find(flag).percentage
end

Missing flag cleanup process leads to permanent conditional logic in codebase. Flags intended as temporary scaffolding become permanent architecture.

# Establish removal policy
class Feature < ApplicationRecord
  scope :stale, -> { where('updated_at < ?', 90.days.ago).where(percentage: 100) }
  
  def self.flag_audit
    stale.each do |flag|
      SlackNotifier.notify("Flag #{flag.name} ready for removal - 100% for 90 days")
    end
  end
end

# Schedule regular audits
# config/schedule.rb
every 1.week do
  runner "Feature.flag_audit"
end

Feature flags in shared libraries or gems create deployment dependencies. The library code contains flags but the configuration lives in the application, causing version compatibility issues.

# Problem: Library code with flag
# my_gem/lib/processor.rb
def process(data)
  if FeatureFlags.enabled?(:fast_processing) # Couples library to app config
    fast_process(data)
  end
end

# Solution: Dependency injection
def process(data, use_fast_processing: false)
  if use_fast_processing
    fast_process(data)
  else
    normal_process(data)
  end
end

# App controls the decision
processor.process(data, use_fast_processing: FeatureFlags.enabled?(:fast_processing))

Reference

Flag Lifecycle Stages

Stage Description Duration Actions
Development Flag created, code merged Days to weeks Create flag disabled, write tests for both paths
Testing Flag enabled in staging Days Verify both enabled and disabled states
Canary Enabled for 1-10% of users Hours to days Monitor metrics, errors, performance
Gradual Rollout Increase percentage incrementally Days to weeks Monitor at each percentage increase
Full Release Enabled for 100% of users Weeks Monitor for regressions
Cleanup Flag removed from code Days Remove flag checks, update tests, delete configuration

Flag Types and Characteristics

Type Purpose Lifetime Targeting Example
Release Control feature rollout Days to weeks Percentage, user groups new_checkout_flow
Experiment A/B testing Weeks to months Random assignment pricing_test_v2
Operational System behavior control Months to permanent Environment, time enable_caching
Permission Authorization logic Permanent User attributes admin_panel_access
Kill Switch Emergency disablement Permanent Global or regional background_jobs
Migration Gradual system changes Weeks to months Percentage, canary new_database_schema

Common Targeting Strategies

Strategy Use Case Implementation Consistency
Boolean Global on/off Single true/false value Perfect
User ID Specific users List of user identifiers Perfect
Percentage Gradual rollout Hash user ID modulo 100 Perfect per user
Random A/B testing Random per request None
Attribute Feature access Rules based on user properties Perfect
Time-based Scheduled features Date/time range checks Perfect
Geographic Regional rollout IP or location data Perfect

Flipper Gate Types

Gate Description Activation Method
Boolean All or nothing enable / disable
Actor Specific entities enable_actor(actor)
Group Named groups enable_group(name)
Percentage of Actors Consistent user percentage enable_percentage_of_actors(percentage)
Percentage of Time Random percentage enable_percentage_of_time(percentage)

Evaluation Performance

Storage Backend Read Latency Write Latency Consistency Use Case
Memory < 1 μs < 1 μs Per-process Testing, single server
Redis 1-5 ms 1-5 ms Strong Multi-server, high traffic
ActiveRecord 5-50 ms 10-100 ms Eventual with cache Standard Rails apps
Remote Service 50-200 ms 50-200 ms Strong Enterprise, multi-region
File System 1-10 ms 10-100 ms None Static config

Flag Management Checklist

Task Frequency Responsible Verification
Create flag in code Per feature Developer Code review
Add flag to config Per feature Developer Deployment check
Enable for testing Per feature QA Test execution
Canary rollout Per feature Operations Metrics review
Increase percentage Per feature Operations Error monitoring
Monitor metrics Continuous Operations Dashboard review
Reach 100% enabled Per feature Operations All environments verified
Schedule removal Per feature Product/Engineering Ticket created
Remove flag checks Per flag Developer Code review
Remove configuration Per flag Operations Config audit

Code Patterns

Pattern Code Template Use Case
Simple Check if FeatureFlags.enabled?(name) Basic on/off
User Context if FeatureFlags.enabled?(name, user) User targeting
Multi-variant case variant(name, user) A/B/n testing
Fallback enabled?(name, user) rescue false Error handling
Group Check if user.in_group?(name) && enabled?(name) Combined logic
Percentage hash(user.id) % 100 < percentage Gradual rollout
Time Window enabled?(name) && Time.current.between?(start, end) Scheduled

Migration Strategy

Phase Flag State Code State Risk Level
Development Created, disabled Both paths exist Low
Testing Enabled in staging Both paths tested Low
Canary 1-5% enabled Monitoring added Medium
Rollout Increasing % Both paths maintained Medium
Complete 100% enabled Flag checks remain Low
Cleanup Removed Old path removed Low