Recommended patterns and conventions for building maintainable feature flag systems with Featury.
Use descriptive, lowercase prefixes with underscores:
# Good
prefix :user_onboarding
prefix :billing_system
prefix :payment_gateway
# Avoid
prefix :uo
prefix :BillingSystem
prefix :payment_gateway_integration_systemGuidelines:
- 2-3 words maximum
- Describes the feature domain
- Uses underscores, not hyphens or camelCase
Use short, descriptive feature names:
# Good
feature :api
feature :webhooks
feature :passage
feature :email_notifications
# Avoid
feature :api_integration
feature :webhook_endpoint_receiver
feature :pGuidelines:
- 1-2 words preferred
- Describes the specific capability
- Combined with prefix should be clear:
:billing_api,:user_onboarding_passage
Follow Rails/Ruby conventions:
# Good
class User::OnboardingFeature < ApplicationFeature
class BillingFeature < ApplicationFeature
class PaymentSystemFeature < ApplicationFeature
# Avoid
class UserOnboarding < ApplicationFeature
class Billing_Feature < ApplicationFeature
class PaymentSystem < ApplicationFeatureGuidelines:
- Use modules for namespacing (
User::,Admin::) - Always suffix with
Feature - Use descriptive names that reflect the domain
Be explicit about resource requirements:
# Good - Clear about requirements
class User::ProfileFeature < ApplicationFeature
resource :user, type: User, option: true # Required
resource :avatar, type: String, option: true, required: false # Optional
end
# Avoid - Ambiguous
class User::ProfileFeature < ApplicationFeature
resource :user, type: User, option: true
resource :avatar, type: String # Is this required?
endOnly use option: true when the resource should be passed to the feature flag system:
# Good - User is passed to Flipper
class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true
action :enabled?, web: :enabled? do |features:, **options|
features.all? { |feature| Flipper.enabled?(feature, *options.values) }
end
end
# Good - Metadata is validated but not passed to Flipper
class AnalyticsFeature < ApplicationFeature
resource :user, type: User, option: true
resource :event_metadata, type: Hash # Only for validation
end
# Avoid - Everything as option when not needed
class BillingFeature < ApplicationFeature
resource :user, type: User, option: true
resource :invoice_id, type: String, option: true # Not needed in Flipper
resource :amount, type: Integer, option: true # Not needed in Flipper
endUse nested: true when resources should flow to nested groups:
# Good - Explicit nesting
class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true, nested: true
group BillingFeature
end
class BillingFeature < ApplicationFeature
resource :user, type: User, option: true
end
# Avoid - Missing nested when required
class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true # BillingFeature won't receive :user
group BillingFeature
endChoose aggregation logic that makes sense for your use case:
# All-must-match for critical features
action :enabled?, web: :enabled? do |features:, **options|
features.all? { |feature| Flipper.enabled?(feature, *options.values) }
end
# Any-can-match for progressive rollout
action :any_enabled?, web: :enabled? do |features:, **options|
features.any? { |feature| Flipper.enabled?(feature, *options.values) }
end
# Count-based for partial availability
action :percentage_enabled, web: :regular do |features:, **options|
enabled_count = features.count { |feature| Flipper.enabled?(feature, *options.values) }
(enabled_count.to_f / features.size * 100).round(2)
endUse consistent web mappings across your application:
# Good - Consistent pattern
class ApplicationFeature < Featury::Base
action :enabled?, web: :enabled? # Read operation
action :disabled?, web: :regular # Read operation (not primary)
action :enable, web: :enable # Write operation
action :disable, web: :disable # Write operation
action :add, web: :regular # Other operation
end
# Avoid - Inconsistent mappings
class ApplicationFeature < Featury::Base
action :enabled?, web: :enabled?
action :enable, web: :regular # Should be :enable
action :disable, web: :enabled? # Should be :disable
endUse callbacks judiciously:
# Good - Specific after callbacks
class ApplicationFeature < Featury::Base
before do |action:, features:|
Rails.logger.info("Feature action: #{action} on #{features}")
end
after :enable, :disable do |action:, features:|
FeatureAuditLog.create!(action: action, features: features)
end
after :enabled?, :disabled? do |action:, features:|
Metrics.track("feature.check", features: features)
end
end
# Avoid - Heavy operations in callbacks
class ApplicationFeature < Featury::Base
before do |action:, features:|
# Expensive external API call
SlackNotifier.notify_all_channels(action, features)
end
endUse conditions for business logic, not configuration:
# Good - Business logic
class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true
condition ->(resources:) { resources.user.onboarding_awaiting? }
feature :passage
end
# Avoid - Configuration logic (use feature flags instead)
class User::OnboardingFeature < ApplicationFeature
resource :user, type: User, option: true
condition ->(resources:) { ENV["ONBOARDING_ENABLED"] == "true" }
feature :passage
endAlways check for presence when using required: false:
# Good
class AnalyticsFeature < ApplicationFeature
resource :user, type: User, option: true, required: false
condition(lambda do |resources:|
resources.user.nil? || resources.user.analytics_enabled?
end)
end
# Avoid - Will raise error if user is nil
class AnalyticsFeature < ApplicationFeature
resource :user, type: User, option: true, required: false
condition ->(resources:) { resources.user.analytics_enabled? }
endGroup related features together:
# Good - Logical grouping
class User::OnboardingFeature < ApplicationFeature
prefix :user_onboarding
feature :passage
feature :tutorial
feature :welcome_email
group BillingFeature # Related to onboarding
group PaymentSystemFeature # Related to billing
end
# Avoid - Unrelated grouping
class User::OnboardingFeature < ApplicationFeature
prefix :user_onboarding
feature :passage
group AdminPanelFeature # Not related to onboarding
group AnalyticsFeature # Not related to onboarding
endKeep all feature classes using the same base:
# Good
class ApplicationFeature < Featury::Base
# Shared action definitions
end
class BillingFeature < ApplicationFeature
end
class PaymentFeature < ApplicationFeature
end
# Avoid - Different bases unless necessary
class BillingFeatureBase < Featury::Base
# Different actions
end
class BillingFeature < BillingFeatureBase
endAlways add descriptions to features and groups:
# Good
class BillingFeature < ApplicationFeature
prefix :billing
feature :api, description: "External billing API integration"
feature :webhooks, description: "Webhook endpoints for billing events"
feature :invoicing, description: "Automated invoice generation"
group PaymentSystemFeature, description: "Payment processing features"
end
# Avoid
class BillingFeature < ApplicationFeature
prefix :billing
feature :api
feature :webhooks
feature :invoicing
group PaymentSystemFeature
endGuidelines:
- Describe what the feature does, not how
- Be concise (one sentence)
- Use consistent terminology
RSpec.describe User::OnboardingFeature do
let(:user) { create(:user) }
describe "condition" do
context "when user is awaiting onboarding" do
before do
allow(user).to receive(:onboarding_awaiting?).and_return(true)
allow(Flipper).to receive(:enabled?).with(:user_onboarding_passage, user).and_return(true)
end
it "proceeds to check feature flags" do
expect(Flipper).to receive(:enabled?).with(:user_onboarding_passage, user)
User::OnboardingFeature.enabled?(user: user)
end
end
context "when user is not awaiting onboarding" do
before { allow(user).to receive(:onboarding_awaiting?).and_return(false) }
it "returns false without checking feature flags" do
expect(Flipper).not_to receive(:enabled?)
expect(User::OnboardingFeature.enabled?(user: user)).to be(false)
end
end
end
endRSpec.describe ApplicationFeature do
describe ".enabled?" do
let(:user) { create(:user) }
before do
class TestFeature < ApplicationFeature
prefix :test
resource :user, type: User, option: true
feature :alpha
feature :beta
end
end
it "returns true when all features are enabled" do
allow(Flipper).to receive(:enabled?).and_return(true)
expect(TestFeature.enabled?(user: user)).to be(true)
end
it "returns false when any feature is disabled" do
allow(Flipper).to receive(:enabled?).with(:test_alpha, user).and_return(true)
allow(Flipper).to receive(:enabled?).with(:test_beta, user).and_return(false)
expect(TestFeature.enabled?(user: user)).to be(false)
end
end
endRSpec.describe User::OnboardingFeature do
describe "resource validation" do
it "raises error when user is not provided" do
expect { User::OnboardingFeature.enabled? }.to raise_error(Servactory::Errors::InputError)
end
it "raises error when user is wrong type" do
expect { User::OnboardingFeature.enabled?(user: "not a user") }.to raise_error(Servactory::Errors::InputError)
end
it "accepts valid user" do
user = create(:user)
expect { User::OnboardingFeature.enabled?(user: user) }.not_to raise_error
end
end
endKeep feature classes focused to reduce the number of feature flag checks:
# Good - Focused feature set
class BillingAPIFeature < ApplicationFeature
prefix :billing
feature :api
feature :webhooks
end
# Avoid - Too many features (consider splitting)
class BillingFeature < ApplicationFeature
prefix :billing
feature :api
feature :webhooks
feature :invoicing
feature :payments
feature :subscriptions
feature :refunds
feature :analytics
feature :reporting
# ... 20 more features
endWhen using .with(), cache the instance if used multiple times:
# Good
feature = User::OnboardingFeature.with(user: user)
if feature.enabled?
feature.disable
log_feature_change(feature)
end
# Avoid - Creating multiple instances
if User::OnboardingFeature.with(user: user).enabled?
User::OnboardingFeature.with(user: user).disable
log_feature_change(User::OnboardingFeature.with(user: user))
endUse the .info API to dynamically discover features and actions:
feature_classes = [BillingFeature, PaymentFeature]
features = feature_classes.map do |klass|
info = klass.info
{
name: klass.name,
features: info.features.all,
actions: info.actions.web.all
}
end
# Use this data to build admin UIs, generate documentation, etc.Create automated documentation from feature definitions:
class FeatureDocGenerator
def self.generate
feature_classes = [User::OnboardingFeature, BillingFeature, PaymentFeature]
feature_classes.each do |klass|
info = klass.info
puts "## #{klass.name}"
puts ""
info.features.all.each do |feature|
puts "- `#{feature.name}`: #{feature.description}"
end
puts ""
end
end
end# Avoid - Too granular
class User::ProfileFeature < ApplicationFeature
feature :edit_name
feature :edit_email
feature :edit_phone
feature :edit_address
# ... 20 more edit features
end
# Good - Appropriate granularity
class User::ProfileFeature < ApplicationFeature
feature :editing
feature :avatar_upload
feature :privacy_settings
end# Avoid - Complex business logic in conditions
class PremiumFeature < ApplicationFeature
condition(lambda do |resources:|
user = resources.user
subscription = user.subscription
return false unless subscription
# 50 lines of complex logic
# ...
end)
end
# Good - Delegate to model methods
class PremiumFeature < ApplicationFeature
condition ->(resources:) { resources.user.eligible_for_premium? }
end
class User < ApplicationRecord
def eligible_for_premium?
# Complex logic here
end
end# Avoid - Circular group dependencies
class FeatureA < ApplicationFeature
group FeatureB
end
class FeatureB < ApplicationFeature
group FeatureA # Circular!
end
# Good - One-way hierarchy
class MainFeature < ApplicationFeature
group FeatureA
group FeatureB
end- Review Examples for real-world patterns
- Explore Integration for framework-specific practices
- See Actions for action design patterns
- Learn about Resources for resource organization