Skip to content

Feature toggles

Feature toggles let us ship unfinished work safely by hiding it behind a runtime switch. Instead of long-lived branches, we merge small increments and control exposure via toggles, so release cadence stays predictable.

Easy Features overview

EasyFeature (see easy_lib/easy_features/easy_feature.rb) is built on top of Flipper. Every toggle has to be registered once. It is registered through the EasyFeatures::Registry, and then evaluated via EasyFeature.enabled?.

Key points:

  • Toggles are registered as symbols (e.g. :ai_project_planner) with metadata such as owners, introduced_in, status, and tracking ref_id.
  • EasyFeature.enabled? delegates to Flipper and caches the decision per actor using EasyFeature::CACHE_TTL to reduce Redis chatter.
  • Unknown keys short-circuit to false to avoid surprises; verification happens through EasyFeatures.known? inside the helper.
  • Redis/Flipper outages are caught and logged, returning false so the platform fails closed.
easy_lib/easy_features/easy_feature.rb
  CACHE_TTL = 5.seconds

  # @param [Symbol, String] key
  # @param [ApplicationRecord] actor, usually User
  # @param [Boolean] default
  # @return [Boolean]
  def self.enabled?(key, actor = User.current)
    return false unless EasyFeatures.known?(key)

    cache_key = "flipper:#{key}:#{actor.flipper_id}"

    Rails.cache.fetch(cache_key, expires_in: CACHE_TTL) do
      Flipper.enabled?(key, actor)
    end
  rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished,
         Redis::BaseError, RedisClient::Error => e
    Rails.logger.error("[feature] flipper error key=#{key} #{e.class}: #{e.message}")
    false
  end

Registering a feature

All toggles must be registered exactly once before they can be queried. Registration usually happens in the engine/plugin entry point or an initializer that runs during boot.

easy_features/registry
1
2
3
4
5
6
7
8
9
EasyFeatures.register(
  key: :ai_project_planner,
  owners: %w[falcon],
  ref_id: "669306",
  introduced_in: "15.5.0",
  status: :experimental,
  target_version: "15.7.0",
  description: "Initial AI-based project planning assistant"
)

Guidelines:

  • key must be a non-empty symbol. Duplicates raise immediately.
  • owners is a list of teams, never individuals.
  • introduced_in and target_version follow the platform version scheme and are validated by Gem::Version.
  • ref_id id of a related task in ESko
  • status experimental, beta, stable, released, use experimental as default. Keep updated as the feature evolves.

Usage patterns

The recommended ergonomics mirror the legacy approach: expose an engine-level helper wrapping EasyFeature.enabled?, then use it in controllers, jobs, or services.

engine module
1
2
3
4
5
6
7
module MyEngine
  class << self
    def my_feature_enabled?
      EasyFeature.enabled?(:my_feature)
    end
  end
end
controller usage
class MyFeatureController < ApplicationController
  before_action :ensure_feature

  private

  def ensure_feature
    render_404 unless MyEngine.my_feature_enabled?
  end
end

Notes:

  • You can enable a feature for explicit actors when needed. The most common actor is User, but it can be Project or other classes as well. The Flipper ID follows the format Class;ID, e.g. User;28628 or Project;42.
  • You can layer environment-specific shortcuts, e.g. always return true in development to avoid toggling during local work.
  • Because results are cached per actor for EasyFeature::CACHE_TTL, avoid manual caching unless you need tighter control.

Naming and conventions

  • Use short, descriptive symbol keys (:issue_suggestions, :new_editor).
  • Prefix helper methods with the feature name plus _enabled? for readability.
  • Keep descriptions and statuses up to date; the registry doubles as living documentation.

Legacy active? API

EasyFeature.active? still exists for historical ENV-only toggles and now emits a deprecation warning urging you toward EasyFeature.enabled?. It delegates to EasyFeatureLegacy and follows the old EASY_FEATURE_*_ACTIVE naming convention.

Use it only when you migrate existing flags. New work must register a feature and call enabled?.

legacy fallback
module MyLegacyEngine
  class << self
    def my_feature_active?
      EasyFeature.active?("EASY_FEATURE_MY_FEATURE_ACTIVE")
    end
  end
end

When you finally convert, swap the helper to enabled?, add the registry entry, and remove the ENV variable gate.