Feature Flags in Rails: Sneller Shippen, Minder Breken
Vorige maand pushte een klant een nieuwe checkout-flow naar productie om 14:00 op een vrijdag. Om 14:15 waren de conversieratio’s met 40% gedaald. De fix kostte drie uur om te schrijven, testen en deployen. Met een feature flag hadden ze de nieuwe flow in minder dan tien seconden kunnen uitschakelen.
Feature flags laten je deployment loskoppelen van release. Je pusht code naar productie, maar bepaalt zelf wie het ziet en wanneer. Als dingen misgaan—en dat gebeurt—draai je aan een schakelaar in plaats van haastig een hotfix te bouwen.
De Simpelste Flag Die Werkt
Voordat je naar een gem grijpt, bedenk hoe weinig je eigenlijk nodig hebt:
# 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 je views en controllers:
<% if FeatureFlag.enabled?(:new_checkout) %>
<%= render "checkout/new_flow" %>
<% else %>
<%= render "checkout/legacy_flow" %>
<% end %>
Dit werkt. Een flag wijzigen vereist een deploy, maar voor veel teams is dat prima. Deploys horen saai te zijn.
Wanneer Je Meer Controle Nodig Hebt
De YAML-aanpak werkt niet meer wanneer je nodig hebt:
- Rollouts per gebruiker of op percentage
- Direct schakelen zonder deploys
- Verschillende flags per omgeving
- Audit trails van wie wat wanneer veranderde
Op dit punt verplaats je de flags naar je 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
# Consistente hashing: dezelfde gebruiker krijgt altijd hetzelfde resultaat
hash = Digest::MD5.hexdigest("#{flag.name}:#{user.id}").to_i(16)
(hash % 100) < flag.rollout_percentage
end
end
De consistente hashing is belangrijk. Zonder zou een gebruiker de feature op de ene pageload zien en op de volgende niet. Verwarde gebruikers dienen bugreports in.
Een Admin Interface Bouwen
Flags in de database hebben een manier nodig om ze te schakelen. Een basis 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 bijgewerkt"
else
render :index, status: :unprocessable_entity
end
end
private
def flag_params
params.require(:feature_flag).permit(:enabled, :rollout_percentage)
end
end
De audit log lijkt overdreven totdat iemand vraagt “wie heeft die feature om 4 uur ‘s nachts aangezet?” Je wilt dat antwoord kunnen geven.
Percentage Rollouts Goed Doen
Uitrollen naar 10% van je gebruikers klinkt eenvoudig. De valkuilen zitten in de details.
Bepaal eerst wat “10%” betekent. Is het 10% van alle gebruikers? 10% van actieve gebruikers? 10% van de requests? Voor user-facing features is percentage van gebruikers logisch. Voor performance-experimenten werkt percentage van requests misschien beter.
Monitor ten tweede de rollout-groep apart:
# In je 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
# nieuwe flow logica
else
# legacy logica
end
end
Als je niet kunt zien of de nieuwe flow beter of slechter presteert, doet de flag zijn werk niet. Ship de metrics samen met de feature. Goede gestructureerde logging maakt het makkelijk om flag-status te correleren met request-uitkomsten.
Oude Flags Opruimen
Feature flags stapelen zich op zoals browsertabs. Een codebase vol achtergelaten conditionals wordt moeilijk leesbaar en nog moeilijker te onderhouden.
Stel een beleid: elke flag krijgt een verwijderdatum. Track het in de metadata:
FeatureFlag.create!(
name: "new_checkout",
enabled: false,
metadata: {
owner: "payments-team",
remove_by: "2026-04-01",
ticket: "PAY-1234"
}
)
Draai een wekelijkse job die Slack pingt wanneer flags hun verwijderdatum passeren. Verouderde flags verdienen aandacht—of ze werkten en moeten permanent worden, of ze werkten niet en moeten weg.
Een pragmatische cleanup rake task:
# lib/tasks/feature_flags.rake
namespace :feature_flags do
desc "Lijst flags voorbij hun verwijderdatum"
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 "VEROUDERD: #{flag.name} (had verwijderd moeten zijn voor #{remove_by})"
end
end
end
Het Flipper Alternatief
Als zelf bouwen voelt als het wiel opnieuw uitvinden, dan handelt Flipper dit goed af:
# 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
# Gebruik
if Flipper.enabled?(:new_checkout, current_user)
# nieuwe flow
end
# Rollout naar 20%
Flipper.enable_percentage_of_actors(:new_checkout, 20)
# Inschakelen voor specifieke gebruikers
Flipper.enable_actor(:new_checkout, beta_user)
Flipper bevat een web UI, percentage rollouts, actor-based targeting en group-based regels. Het is volwassen en goed gedocumenteerd. Voor de meeste Rails-apps is het de juiste keuze.
Flags in Tests
Feature flags in tests creëren flaky specs als je niet oplet. De flag-status lekt tussen voorbeelden.
Reset flags in je test setup:
# spec/rails_helper.rb
RSpec.configure do |config|
config.before(:each) do
FeatureFlag.update_all(enabled: false, rollout_percentage: 0)
end
end
Voor specs die een flag nodig hebben:
describe "nieuwe checkout flow" do
before do
FeatureFlag.find_or_create_by!(name: "new_checkout")
.update!(enabled: true)
end
it "toont de nieuwe betaalopties" do
# ...
end
end
Of maak een helper die de before/after afhandelt:
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 "toont de nieuwe checkout wanneer ingeschakeld" do
with_feature(:new_checkout) do
visit checkout_path
expect(page).to have_content("Nieuwe checkout ervaring")
end
end
Wanneer Flags Meer Pijn Dan Plezier Geven
Feature flags voegen complexiteit toe. Elk conditioneel pad verdubbelt de states waarin je code kan zijn. Twee flags betekent vier combinaties. Vijf flags betekent tweeëndertig.
Vermijd flags voor:
- Database schema wijzigingen (die hebben goede zero-downtime migraties nodig, geen flags)
- Bugfixes (fix gewoon de bug)
- Wijzigingen die niemand hoeft terug te draaien (hernoemen van een interne class)
Gebruik flags voor:
- User-facing features met risico
- Geleidelijke rollouts om performance-issues te vangen
- A/B tests met meetbare uitkomsten
- Features die je snel moet kunnen killen
Het doel is vertrouwen, niet dekking. Flag de dingen die je bang maken, niet alles.
De Keuze Maken
Begin met de YAML-aanpak als je vaak deployt en minimale infrastructuur wilt. Stap over naar database-backed flags wanneer je instant schakelen of user-based rollouts nodig hebt. Overweeg Flipper wanneer je liever configureert dan codeert.
Wat je ook kiest, houd de interface consistent. FeatureFlag.enabled?(:name) of Flipper.enabled?(:name) overal. Wanneer het tijd is om een flag te verwijderen, moet een project-brede zoekopdracht elk gebruik vinden.
Feature flags voorkomen niet alle productie-incidenten. Ze maken het herstel om 3 uur ‘s nachts wel sneller. Voor een vrijdagmiddag-deployment is dat veel waard.
Veelgestelde Vragen
Wat is het verschil tussen feature flags en A/B-testen?
Feature flags bepalen of een gebruiker een feature überhaupt ziet. A/B-testen meten welke variant beter presteert. In de praktijk zijn feature flags het mechanisme dat A/B-testen mogelijk maakt — je gebruikt de flag om verkeer te splitsen en je analytics om het resultaat te meten. Je kunt A/B-testen doen zonder feature flags, maar flags maken het veel eenvoudiger om de rollout te beheren.
Moeten feature flags in de database of in configuratiebestanden staan?
Begin met configuratiebestanden (YAML) als je team regelmatig deployt en je alleen aan/uit-schakelaars nodig hebt. Stap over naar database-backed flags wanneer je direct moet kunnen schakelen zonder deploys, percentage-rollouts nodig hebt, of per-gebruiker targeting wilt. De database-aanpak voegt een query per flag-check toe, dus overweeg flag-waarden te cachen met een korte TTL.
Hoeveel feature flags is te veel?
Er is geen magisch getal, maar als je meer dan 10-15 actieve flags hebt, accumuleert je codebase snel complexiteit. Elke flag verdubbelt de mogelijke codepaden. Stel verwijderdata in op elke flag en handhaaf opruiming. De meeste flags zouden weken moeten bestaan, niet maanden.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
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