35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Feature Flags in Rails: Ship Faster, Break Less

Feature Flags in Rails: Ship Faster, Break Less

TTB Software
rails, devops
How to implement feature flags in Rails applications for safer deployments, gradual rollouts, and the ability to kill a bad feature without rolling back.

Last month, a client pushed a new checkout flow to production at 2 PM on a Friday. By 2:15 PM, conversion rates had dropped 40%. The fix took three hours to write, test, and deploy. With a feature flag, they could have disabled the new flow in under ten seconds.

Feature flags let you decouple deployment from release. You push code to production but control who sees it and when. When things go wrong—and they will—you flip a switch instead of scrambling for a hotfix.

The Simplest Flag That Works

Before reaching for a gem, consider how little you actually need:

# config/feature_flags.yml
production:
  new_checkout: false
  beta_dashboard: true
  
# app/models/feature_flag.rb
class FeatureFlag
  def self.enabled?(flag)
    config = Rails.application.config_for(:feature_flags)
    config[flag.to_s] == true
  end
end

In your views and controllers:

<% if FeatureFlag.enabled?(:new_checkout) %>
  <%= render "checkout/new_flow" %>
<% else %>
  <%= render "checkout/legacy_flow" %>
<% end %>

This works. Changing a flag requires a deploy, but for many teams that’s fine. Deploys should be boring anyway.

When You Need More Control

The YAML approach falls apart when you need:

  • Per-user or percentage-based rollouts
  • Instant toggling without deploys
  • Different flags per environment
  • Audit trails of who changed what

At this point, move the flags to your database:

# db/migrate/create_feature_flags.rb
class CreateFeatureFlags < ActiveRecord::Migration[7.1]
  def change
    create_table :feature_flags do |t|
      t.string :name, null: false, index: { unique: true }
      t.boolean :enabled, default: false
      t.integer :rollout_percentage, default: 0
      t.jsonb :metadata, default: {}
      t.timestamps
    end
  end
end

# app/models/feature_flag.rb
class FeatureFlag < ApplicationRecord
  def self.enabled?(name, user: nil)
    flag = find_by(name: name)
    return false unless flag
    return flag.enabled if user.nil?
    
    flag.enabled && within_rollout?(flag, user)
  end
  
  def self.within_rollout?(flag, user)
    return true if flag.rollout_percentage >= 100
    return false if flag.rollout_percentage <= 0
    
    # Consistent hashing: same user always gets same result
    hash = Digest::MD5.hexdigest("#{flag.name}:#{user.id}").to_i(16)
    (hash % 100) < flag.rollout_percentage
  end
end

The consistent hashing matters. Without it, a user might see the feature on one page load and not the next. Confused users file bug reports.

Building an Admin Interface

Flags in the database need a way to toggle them. A basic admin controller:

class Admin::FeatureFlagsController < AdminController
  def index
    @flags = FeatureFlag.order(:name)
  end
  
  def update
    @flag = FeatureFlag.find(params[:id])
    old_state = @flag.enabled
    
    if @flag.update(flag_params)
      AuditLog.record(
        user: current_admin,
        action: "feature_flag_changed",
        details: { 
          flag: @flag.name, 
          from: old_state, 
          to: @flag.enabled 
        }
      )
      redirect_to admin_feature_flags_path, notice: "Flag updated"
    else
      render :index, status: :unprocessable_entity
    end
  end
  
  private
  
  def flag_params
    params.require(:feature_flag).permit(:enabled, :rollout_percentage)
  end
end

The audit log seems like overhead until the day someone asks “who turned on that feature at 4 AM?” You’ll want that answer.

Percentage Rollouts Done Right

Rolling out to 10% of users sounds straightforward. The gotchas hide in the details.

First, decide what “10%” means. Is it 10% of all users? 10% of active users? 10% of requests? For user-facing features, percentage of users makes sense. For performance experiments, percentage of requests might work better.

Second, monitor the rollout cohort separately:

# In your checkout controller
def create
  using_new_checkout = FeatureFlag.enabled?(:new_checkout, user: current_user)
  
  StatsD.increment(
    "checkout.attempt",
    tags: ["new_flow:#{using_new_checkout}"]
  )
  
  if using_new_checkout
    # new flow logic
  else
    # legacy logic
  end
end

If you can’t see whether the new flow performs better or worse, the flag isn’t doing its job. Ship the metrics alongside the feature. Good structured logging makes it easy to correlate flag state with request outcomes.

Cleaning Up Old Flags

Feature flags accumulate like browser tabs. A codebase littered with abandoned conditionals becomes hard to read and harder to maintain.

Set a policy: every flag gets a removal date. Track it in the metadata:

FeatureFlag.create!(
  name: "new_checkout",
  enabled: false,
  metadata: { 
    owner: "payments-team",
    remove_by: "2026-04-01",
    ticket: "PAY-1234"
  }
)

Run a weekly job that pings Slack when flags pass their removal date. Stale flags need attention—either they worked and should be permanent, or they didn’t and should go.

A pragmatic cleanup rake task:

# lib/tasks/feature_flags.rake
namespace :feature_flags do
  desc "List flags past their removal date"
  task stale: :environment do
    today = Date.current
    
    FeatureFlag.find_each do |flag|
      remove_by = flag.metadata["remove_by"]&.to_date
      next unless remove_by && remove_by < today
      
      puts "STALE: #{flag.name} (should have been removed by #{remove_by})"
    end
  end
end

The Flipper Alternative

If building your own feels like reinventing wheels, Flipper handles this well:

# Gemfile
gem "flipper"
gem "flipper-active_record"
gem "flipper-ui"

# config/initializers/flipper.rb
Flipper.configure do |config|
  config.default do
    adapter = Flipper::Adapters::ActiveRecord.new
    Flipper.new(adapter)
  end
end

# Usage
if Flipper.enabled?(:new_checkout, current_user)
  # new flow
end

# Rollout to 20%
Flipper.enable_percentage_of_actors(:new_checkout, 20)

# Enable for specific users
Flipper.enable_actor(:new_checkout, beta_user)

Flipper includes a web UI, percentage rollouts, actor-based targeting, and group-based rules. It’s mature and well-documented. For most Rails apps, it’s the right choice.

Flags in Tests

Feature flags in tests create flaky specs if you’re not careful. The flag state bleeds between examples.

Reset flags in your test setup:

# spec/rails_helper.rb
RSpec.configure do |config|
  config.before(:each) do
    FeatureFlag.update_all(enabled: false, rollout_percentage: 0)
  end
end

For specs that need a flag enabled:

describe "new checkout flow" do
  before do
    FeatureFlag.find_or_create_by!(name: "new_checkout")
      .update!(enabled: true)
  end
  
  it "shows the new payment options" do
    # ...
  end
end

Or create a helper that handles the before/after:

def with_feature(name, enabled: true)
  flag = FeatureFlag.find_or_create_by!(name: name)
  original = flag.enabled
  flag.update!(enabled: enabled)
  yield
ensure
  flag.update!(enabled: original)
end

it "shows the new checkout when enabled" do
  with_feature(:new_checkout) do
    visit checkout_path
    expect(page).to have_content("New checkout experience")
  end
end

When Flags Hurt More Than Help

Feature flags add complexity. Every conditional path doubles the states your code can be in. Two flags means four combinations. Five flags means thirty-two.

Avoid flags for:

  • Database schema changes (those need proper zero-downtime migrations, not flags)
  • Bug fixes (just fix the bug)
  • Changes nobody needs to roll back (renaming an internal class)

Use flags for:

  • User-facing features with risk
  • Gradual rollouts to catch performance issues
  • A/B tests with measurable outcomes
  • Features you might need to kill quickly

The goal is confidence, not coverage. Flag the things that scare you, not everything.

Making the Call

Start with the YAML approach if you deploy frequently and want minimal infrastructure. Move to database-backed flags when you need instant toggling or user-based rollouts. Consider Flipper when you’d rather configure than code.

Whatever you choose, keep the interface consistent. FeatureFlag.enabled?(:name) or Flipper.enabled?(:name) everywhere. When it’s time to remove a flag, a project-wide search should find every usage.

Feature flags won’t prevent all production incidents. They will make the 3 AM recovery faster. For a Friday afternoon deployment, that’s worth a lot.

Frequently Asked Questions

What’s the difference between feature flags and A/B testing?

Feature flags control whether a user sees a feature at all. A/B testing measures which variant performs better. In practice, feature flags are the mechanism that enables A/B tests — you use the flag to split traffic and your analytics to measure the outcome. You can run A/B tests without feature flags, but flags make it much easier to control the rollout.

Should feature flags live in the database or in configuration files?

Start with configuration files (YAML) if your team deploys frequently and you only need on/off toggles. Move to database-backed flags when you need instant toggling without deploys, percentage-based rollouts, or per-user targeting. The database approach adds a query per flag check, so consider caching flag values with a short TTL.

How many feature flags is too many?

There’s no magic number, but if you have more than 10-15 active flags, your codebase is accumulating complexity fast. Each flag doubles the possible code paths. Set removal dates on every flag and enforce cleanup. Most flags should live for weeks, not months.

T

About the Author

Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation