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
Ruby on Rails Feature Flags: Complete Guide met Flipper, Rollout en Custom Redis Implementatie

Ruby on Rails Feature Flags: Complete Guide met Flipper, Rollout en Custom Redis Implementatie

Roger Heykoop
Ruby on Rails, DevOps
Ship Rails features veilig met feature flags. Flipper setup, custom Redis flags, percentage rollouts, CI/CD integratie, teststrategieën en veelvoorkomende valkuilen.

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.

#rails #feature-flags #flipper #rollout #redis #ci-cd #ruby #deployment
R

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 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