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 |