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: Sneller Shippen, Minder Breken

Feature Flags in Rails: Sneller Shippen, Minder Breken

TTB Software
rails, devops
Hoe je feature flags implementeert in Rails-applicaties voor veiligere deployments, geleidelijke rollouts en de mogelijkheid om een slechte feature uit te schakelen zonder rollback.

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.

T

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