Ruby on Rails Feature Flags: Complete Guide met Flipper, Rollout en Custom Redis Implementatie
Drie weken geleden keek ik toe terwijl een deploy een checkout-flow veertig minuten plat legde. Het team had de fix binnen tien minuten klaar, maar hun CI-pipeline had nog eens dertig minuten nodig om het naar productie te krijgen. Eén toggle in Redis had de feature in minder dan een seconde kunnen uitschakelen. Die toggle hadden ze niet.
Feature flags zijn het verschil tussen “we hebben over een uur een fix live” en “het staat al uit.” Na negentien jaar Rails heb ik elke aanpak gezien — YAML-bestanden, database-backed flags, Redis, Flipper, Rollout, en genoeg handgebouwde oplossingen die nooit handgebouwd hadden mogen worden. Hier is wat echt werkt, wanneer je wat moet gebruiken, en de valkuilen die zelfs ervaren teams vangen.
Waarom Feature Flags Belangrijker Zijn Dan Je Denkt
Feature flags ontkoppelen deployment van release. Je pusht code naar productie, maar de feature blijft donker totdat je de schakelaar omzet. Dit klinkt simpel. De implicaties zijn dat niet.
Zonder flags is elke deploy een release. Je CI/CD-pipeline is het enige tussen “gemerged naar main” en “gebruikers zien het.” Dat legt enorme druk op code review en staging-omgevingen om alles te vangen. Dat lukt niet. Staging matcht nooit met productie. Code review vangt logische bugs, geen load-gerelateerde failures.
Met flags deploy je de code, schakel je het eerst in voor je team, en rol je het uit naar 5% van de gebruikers. Als foutpercentages pieken, schakel je het uit. Geen rollback, geen hotfix-branch, geen emergency deploy. De code blijft gedeployed — hij stopt alleen met uitvoeren.
Dit verandert hoe je over risico denkt. Features die anders hadden gewacht op de volgende “grote release” kun je incrementeel shippen. De metaprogramming-technieken die veel flag-libraries aansturen worden praktische tools in plaats van academische curiositeiten.
Flipper: De Juiste Default voor de Meeste Rails Apps
Als je fris begint, gebruik Flipper. Het is volwassen, goed onderhouden, en dekt 90% van wat je nodig hebt zonder zelf flag-infrastructuur te schrijven.
Setup
# Gemfile
gem "flipper"
gem "flipper-active_record" # of flipper-redis, flipper-mongo
gem "flipper-ui" # optioneel web dashboard
# terminal
bundle install
bin/rails generate flipper:active_record
bin/rails db:migrate
De migratie maakt een flipper_features en flipper_gates tabel aan. Dat is je flag-opslag.
Basisgebruik
# Globaal inschakelen
Flipper.enable(:new_search)
# Checken
if Flipper.enabled?(:new_search)
render_new_search
end
# Uitschakelen
Flipper.disable(:new_search)
In controllers wrap ik dit in een helper:
# app/controllers/application_controller.rb
private
def feature?(name)
Flipper.enabled?(name, current_user)
end
helper_method :feature?
Nu blijven views schoon:
<% if feature?(:new_search) %>
<%= render "search/redesigned" %>
<% else %>
<%= render "search/current" %>
<% end %>
Actor-Based Targeting
De echte kracht van Flipper is het targeten van specifieke gebruikers of groepen. Je user-model moet reageren op flipper_id:
class User < ApplicationRecord
# Flipper gebruikt dit om actors te identificeren
# ActiveRecord models werken out of the box — flipper_id defaultt naar
# "User;#{id}" wat uniek en stabiel is
end
Nu kun je precies targeten:
# Inschakelen voor één gebruiker (beta tester, interne QA)
Flipper.enable_actor(:new_checkout, User.find_by(email: "beta@example.com"))
# Inschakelen voor een percentage gebruikers
Flipper.enable_percentage_of_actors(:new_checkout, 10)
# Inschakelen voor een groep
Flipper.register(:staff) do |actor|
actor.respond_to?(:staff?) && actor.staff?
end
Flipper.enable_group(:new_checkout, :staff)
Percentage rollouts gebruiken consistent hashing — dezelfde gebruiker krijgt altijd hetzelfde resultaat. Geen geflakker tussen pagina-loads.
Flipper UI
Mount het dashboard in je routes:
# config/routes.rb
Rails.application.routes.draw do
constraints ->(req) { req.env["warden"].user&.admin? } do
mount Flipper::UI.app(Flipper) => "/flipper"
end
end
De constraint is belangrijk. Zonder kan iedereen je features togglen. Ik heb productie-features zien uitschakelen door crawlers die de Flipper UI raakten omdat iemand authenticatie vergat. Wees niet dat team.
Custom Redis Implementatie: Wanneer Je Rauwe Snelheid Nodig Hebt
Flipper met de ActiveRecord-adapter voegt een database-query toe per flag-check. Met caching is dat meestal prima. Maar als je flags checkt in hot paths — middleware, API endpoints die duizenden requests per seconde verwerken — wil je misschien Redis direct.
# app/services/feature_flags.rb
class FeatureFlags
REDIS_PREFIX = "feature_flags:"
CACHE_TTL = 30 # seconden
class << self
def enabled?(flag, actor: nil)
return false unless flag_active?(flag)
return true if actor.nil?
within_rollout?(flag, actor)
end
def enable(flag)
redis.set("#{REDIS_PREFIX}#{flag}", "1")
invalidate_cache(flag)
end
def disable(flag)
redis.set("#{REDIS_PREFIX}#{flag}", "0")
invalidate_cache(flag)
end
def set_rollout(flag, percentage)
redis.hset("#{REDIS_PREFIX}#{flag}:config", "rollout", percentage)
invalidate_cache(flag)
end
private
def flag_active?(flag)
Rails.cache.fetch("ff:#{flag}", expires_in: CACHE_TTL) do
redis.get("#{REDIS_PREFIX}#{flag}") == "1"
end
end
def within_rollout?(flag, actor)
percentage = redis.hget("#{REDIS_PREFIX}#{flag}:config", "rollout").to_i
return true if percentage >= 100
return false if percentage <= 0
actor_id = actor.respond_to?(:id) ? actor.id : actor.to_s
Digest::SHA256.hexdigest("#{flag}:#{actor_id}").first(8).to_i(16) % 100 < percentage
end
def invalidate_cache(flag)
Rails.cache.delete("ff:#{flag}")
end
def redis
@redis ||= Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1"))
end
end
end
Gebruik is identiek:
if FeatureFlags.enabled?(:fast_search, actor: current_user)
# nieuw pad
end
De 30-seconden cache TTL betekent dat flag-wijzigingen binnen een halve minuut propageren. Voor de meeste use cases is dat snel genoeg. Als je directe propagatie nodig hebt, laat de cache vallen of gebruik Redis pub/sub om te invalideren over app servers.
Wanneer Redis Beter Is Dan ActiveRecord
| Scenario | ActiveRecord | Redis |
|---|---|---|
| Flag checks per request | 1-3 | 10+ |
| p99 latency-eis | <100ms | <10ms |
| Flag-wijzigingsfrequentie | Minuten | Sub-seconde |
| Infrastructuur | Al Postgres | Al Redis |
Als je al Redis draait voor caching of Sidekiq, zijn de marginale kosten van flag-opslag nul. Als je enige datastore Postgres is, is Redis toevoegen alleen voor flags overkill — gebruik Flipper met ActiveRecord en klaar.
De Rollout Gem: Lichtgewicht Alternatief
Rollout is ouder en simpeler dan Flipper. Het is Redis-backed, heeft geen UI, en doet één ding: percentage-based feature rollouts.
# Gemfile
gem "rollout"
# config/initializers/rollout.rb
$rollout = Rollout.new(Redis.new)
# Activeer voor een percentage gebruikers
$rollout.activate_percentage(:new_dashboard, 25)
# Check
if $rollout.active?(:new_dashboard, current_user)
# toon nieuw dashboard
end
# Activeer voor een specifieke gebruiker
$rollout.activate_user(:new_dashboard, admin_user)
# Volledig deactiveren
$rollout.deactivate(:new_dashboard)
Rollout vereist dat je user-object reageert op id. Dat is het enige contract.
Ik gebruik Rollout nog steeds in kleinere apps waar Flipper’s UI en groepssysteem als overhead aanvoelen. Voor alles met meer dan twee developers rechtvaardigen Flipper’s audit trail en dashboard de extra setup.
Feature Flags Testen Zonder Gek Te Worden
Feature flags in tests zijn een bron van flaky specs als je niet oppast. Flag-state lekt tussen voorbeelden, en opeens is je CI rood om redenen die niemand lokaal kan reproduceren.
Met Flipper
# spec/support/flipper.rb
RSpec.configure do |config|
config.before(:each) do
Flipper.features.each(&:disable)
end
end
# In specs
describe "new search", type: :feature do
before { Flipper.enable(:new_search) }
it "shows redesigned results" do
visit search_path(q: "rails")
expect(page).to have_css(".search-v2")
end
end
Met Custom Flags
# spec/support/feature_flags.rb
module FeatureFlagHelpers
def with_feature(flag, actor: nil)
FeatureFlags.enable(flag)
yield
ensure
FeatureFlags.disable(flag)
end
end
RSpec.configure do |config|
config.include FeatureFlagHelpers
end
# In specs
it "rendert de nieuwe checkout" do
with_feature(:new_checkout) do
visit checkout_path
expect(page).to have_content("Express checkout")
end
end
Beide Paden Testen
Dit is het deel dat teams overslaan en betreuren. Elke flag creëert twee codepaden. Test beide:
describe CheckoutController do
context "met nieuwe checkout ingeschakeld" do
before { Flipper.enable(:new_checkout) }
it "verwerkt betaling via Stripe v2" do
post :create, params: { amount: 1000 }
expect(StripeV2::Charge).to have_received(:create)
end
end
context "met nieuwe checkout uitgeschakeld" do
before { Flipper.disable(:new_checkout) }
it "verwerkt betaling via legacy gateway" do
post :create, params: { amount: 1000 }
expect(LegacyGateway).to have_received(:charge)
end
end
end
CI/CD Integratie
Feature flags veranderen hoe je deployment-pipeline werkt. Dit moet je automatiseren.
Flag Validatie in CI
Voeg een rake task toe die verifieert dat flag-referenties matchen met wat geregistreerd is:
# lib/tasks/feature_flags.rake
namespace :feature_flags do
desc "Check for references to unregistered flags"
task validate: :environment do
# Vind alle flag-referenties in code
flag_refs = `grep -rhoP 'Flipper\\.enabled\\?\\(:\\K[a-z_]+' app/`.split.uniq
flag_refs += `grep -rhoP 'feature\\?\\(:\\K[a-z_]+' app/`.split.uniq
registered = Flipper.features.map(&:name).map(&:to_s)
orphans = flag_refs - registered
if orphans.any?
warn "WARNING: Referenced flags not registered in Flipper: #{orphans.join(', ')}"
warn "Run: #{orphans.map { |f| "Flipper.add(:#{f})" }.join('; ')}"
end
end
end
Draai dit in CI na db:migrate:
# .github/workflows/ci.yml
- name: Validate feature flags
run: bundle exec rake feature_flags:validate
Stale Flag Detectie
Flags stapelen op. Stel expiratie-metadata in en handhaaf het:
# lib/tasks/feature_flags.rake
namespace :feature_flags do
desc "Report stale feature flags"
task stale: :environment do
Flipper.features.each do |feature|
created = feature.created_at || Time.zone.parse("2026-01-01")
age_days = (Time.current - created).to_i / 86400
if age_days > 30 && feature.boolean_value
puts "STALE (#{age_days}d, fully enabled): #{feature.name} — verwijder flag en behoud code"
elsif age_days > 60
puts "ANCIENT (#{age_days}d): #{feature.name} — verwijder flag en waarschijnlijk ook de code"
end
end
end
end
Deploy-Time Flag Seeding
Nieuwe flags moeten bestaan voordat de code die ze checkt live gaat. Voeg flag seeding toe aan je deploy-proces:
# db/seeds/feature_flags.rb
flags = %w[
new_search
express_checkout
ai_recommendations
]
flags.each do |flag|
Flipper.add(flag) unless Flipper.exist?(flag)
end
# In je deploy script of CI
- name: Seed feature flags
run: bundle exec rails runner 'load "db/seeds/feature_flags.rb"'
Dit voorkomt de race condition waarbij je app een flag checkt die nog niet aangemaakt is. Flipper retourneert false voor onbekende flags, wat veilig is — maar het is beter om expliciet te zijn.
Veelvoorkomende Valkuilen
1. Flag Sprawl
Ik heb ooit een codebase geaudit met 87 actieve feature flags. Niemand wist welke nog relevant waren. De if/else-branches hadden if/else-branches erin. Testen was een grap — je zou 2^87 combinaties nodig hebben voor volledige coverage.
Oplossing: Elke flag krijgt een eigenaar en een verwijderdatum. Zet het in een comment naast de flag-check:
# FLAG: new_search | owner: search-team | remove-by: 2026-05-01 | ticket: SRCH-442
if feature?(:new_search)
Een linter of grep kan flags voorbij hun datum vangen.
2. Flags in Migraties
Gebruik geen feature flags in database-migraties. Migraties draaien één keer en zijn permanent. Een flag die vandaag uit staat kan morgen aan staan, maar je migratie is al gedraaid. Gebruik zero-downtime migratietechnieken in plaats daarvan.
3. Geneste Flag Dependencies
# Doe dit niet
if feature?(:new_checkout) && feature?(:stripe_v2) && !feature?(:legacy_override)
# welke combinatie van drie booleans brengt je hier?
end
Als flag B alleen zinvol is wanneer flag A ingeschakeld is, maak er dan één flag van of gebruik Flipper-groepen. Combinatorische flags zijn een onderhoudsnachtmerrie.
4. De Else-Branch Vergeten
# Gevaarlijk: wat gebeurt er als de flag uit staat?
def calculate_shipping
if feature?(:new_shipping_calculator)
NewShippingCalculator.call(order)
end
# nil — je verzending is nu gratis. Gefeliciteerd.
end
Behandel altijd beide branches. Als het flag-uit-pad “doe niets” is, maak dat expliciet met een comment.
5. Flags Checken in Loops
# Traag: raakt cache/Redis bij elke iteratie
orders.each do |order|
if FeatureFlags.enabled?(:new_pricing, actor: order.user)
# ...
end
end
# Beter: check één keer, partitioneer
enabled_users = Set.new(users_with_feature(:new_pricing).pluck(:id))
orders.each do |order|
if enabled_users.include?(order.user_id)
# ...
end
end
De Keuze Maken
| Team/App Grootte | Aanbeveling |
|---|---|
| Solo/prototype | YAML config of ENV vars |
| Klein team, simpele behoeften | Rollout gem + Redis |
| De meeste Rails apps | Flipper + ActiveRecord |
| High-traffic, latency-gevoelig | Flipper + Redis adapter, of custom Redis |
| Enterprise, audit-eisen | Flipper Cloud of LaunchDarkly |
Begin simpel. Je kunt altijd migreren van Rollout naar Flipper — de interface is bijna identiek. Van YAML naar database-backed flags is een grotere sprong, dus maak die stap vroeg als je percentage rollouts verwacht.
Feature flags zijn niet alleen een deployment-gemak. Het is een fundamenteel andere manier van software shippen. Het team dat maandag een feature kan inschakelen voor 1% van de gebruikers, de metrics kan bekijken, woensdag kan uitbreiden naar 10%, en vrijdag naar 100% kan gaan, shipt met een vertrouwen dat geen enkele staging-omgeving kan evenaren.
Hulp nodig bij het implementeren van feature flags in je Rails app, of het opruimen van een codebase bedolven onder jarenlange stale toggles? TTB Software shipt al negentien jaar Rails naar productie. We hebben elk flag-patroon gezien — en elke flag-ramp.
Veelgestelde Vragen
Welke feature flag gem moet ik gebruiken voor een nieuwe Rails 8 app?
Flipper. Het heeft de breedste adapter-ondersteuning (ActiveRecord, Redis, Mongo), een ingebouwde web UI, actor- en groep-targeting, en actief onderhoud. Tenzij je een specifieke reden hebt om custom te gaan, bespaart Flipper je het opnieuw uitvinden van het wiel.
Kan ik feature flags gebruiken met Rails fragment caching?
Ja, maar je moet de flag-state opnemen in de cache key. Anders serveren gecachte fragmenten verouderde content wanneer een flag verandert:
<% cache [current_user, feature?(:new_layout), "sidebar"] do %>
<%= render feature?(:new_layout) ? "sidebar_v2" : "sidebar" %>
<% end %>
Hoe beïnvloeden feature flags de performance in productie?
Met een ActiveRecord-adapter is elke flag-check een database-query (typisch <1ms met juiste indexes). Met Redis is het een netwerk-roundtrip (~0,1-0,5ms). Met in-memory caching erop is het effectief gratis na de eerste check. De meeste apps kunnen 10+ flags per request checken zonder meetbare impact.
Moet ik API endpoints anders feature-flaggen dan UI features?
Het check-mechanisme is hetzelfde, maar de response is belangrijk. Voor UI toon of verberg je elementen. Voor APIs, retourneer een zinvolle response wanneer de feature uit staat — een juiste foutcode, geen 500. Documenteer geflagde endpoints in je API changelog zodat consumenten niet verrast worden.
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