DEV Community

Vlad Hilko
Vlad Hilko

Posted on

How to add Feature Flags in Ruby on Rails?

Overview

In this article, we are going to delve into the area of Feature Flags. We will explain what a feature flag is, why we need it, and how we can use it. We will discuss how to create feature flags and explore the advantages and disadvantages they offer. Additionally, we will explore different feature release strategies that can be safely employed in your Rails application. Finally, we will consider various improvements that can be applied to enhance the reliability of feature flags.

Definition

In simple terms, a feature flag is a technique used in software development to enable or disable specific features at runtime. Feature flags act as switches that control the availability and visibility of certain features within an application without the need for redeploying or modifying the code. Using feature flags has its pros and cons, let's consider them:

Advantages:

  • Risk Mitigation

Easily disable features in case of unexpected problems or errors.

  • A/B Testing and Experimentation

Conduct experiments and compare different feature variations to make informed decisions.

  • Controlled Rollouts

Allows testing the feature on different platforms (staging/sandbox) before enabling it in production.

  • Speed of merging

Developers can work on small pull requests that are merged frequently into the main code branch.

Problems:

  • Increased Complexity

Increases code loading and cognitive load due to the need for additional conditional checks in the code.

  • Technical Debt

If feature flags are not appropriately managed and cleaned up, they can become deprecated or unnecessary, resulting in increased code complexity and maintenance overhead.

  • Testing Overhead

The presence of multiple feature variations requires thorough testing of each flag's behavior and interactions, potentially increasing the testing effort and complexity.

It's important to remember that a feature flag is a temporary solution that should be removed after the feature is fully implemented, tested, delivered, and approved by clients.

Implementation

We're going to use the flipper gem. Let's take a look at how to use it properly.

Installation

First of all, we need to install this gem.

# Gemfile

gem 'flipper'
Enter fullscreen mode Exit fullscreen mode

And then execute:

bundle
Enter fullscreen mode Exit fullscreen mode

The gem provides interfaces for the following most important features:

  • Enabling/Disabling the feature
  • Checking if the feature is enabled or not
  • Showing the list of all features
# rails c 

Flipper.features
# #<Set: {}>
Flipper.enable :search
# #<Set: {#<Flipper::Feature:72040 name=:search, state=:on, enabled_gate_names=[:boolean], adapter=:memoizable>}>
Flipper.enabled? :search
# true
Flipper.disable :search
# #<Set: {#<Flipper::Feature:72040 name=:search, state=:off, enabled_gate_names=[], adapter=:memoizable>}>
Flipper.enabled? :search
# false
Enter fullscreen mode Exit fullscreen mode

The problem with the current approach is that the data stored in memory will be lost after reloading the Rails console. Therefore, we need to determine where exactly we want to store this data. Flipper provides several available options for storing the value, called adapters. In this case, we're going to use flipper-active_record as it is the most familiar one.

Add ActiveRecord adapter

To use the ActiveRecord adapter, we need to install the gem:

# Gemfile

gem 'flipper-active_record'
Enter fullscreen mode Exit fullscreen mode

And then execute:

bundle
Enter fullscreen mode Exit fullscreen mode

After the gem is installed, we need to generate a migration where our data will be stored:

rails g flipper:active_record
Enter fullscreen mode Exit fullscreen mode

And then execute:

rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Let's see how it works in the Rails console:

# rails c 

Flipper.features
# #<Set: {}>
Flipper.enable :search
# This command creates the following records in the database:

# [
#   #<Flipper::Adapters::ActiveRecord::Feature:0x000000011672da78
#   id: 1,
#   key: "search",
#   created_at: Sun, 09 Jul 2023 13:08:14.499774000 UTC +00:00,
#   updated_at: Sun, 09 Jul 2023 13:08:14.499774000 UTC +00:00>
# ]
# [
#   #<Flipper::Adapters::ActiveRecord::Gate:0x00000001166ddbb8
#   id: 1,
#   feature_key: "search",
#   key: "boolean",
#   value: "true",
#   created_at: Sun, 09 Jul 2023 13:08:14.527445000 UTC +00:00,
#   updated_at: Sun, 09 Jul 2023 13:08:14.527445000 UTC +00:00>
# ]

Flipper.enabled? :search
# true
Enter fullscreen mode Exit fullscreen mode

That's it.

Feature Release Strategies

Feature release strategies refer to the approaches and techniques used to release new features or updates in software development.

Here are some common feature release strategies:

  • Big Bang Release
  • Incremental Release
  • Feature Flags/Toggles
  • Canary Release
  • Phased Rollout
  • Fault tolerance

We will discuss these strategies using a simple abstract example of changing the background color of our website to red. Let's take a closer look at each of them and provide examples of their implementation in Ruby.

Big Bang Release

The Big Bang Release strategy states that all changes need to be deployed at once as a single, comprehensive update. This strategy does not require the use of feature flags. For example, if you had the following code in your application:

# change_background_color.rb

ChangeBackgroundColor.call('blue')
Enter fullscreen mode Exit fullscreen mode

The Big Bang Strategy will replace this code all at once wherever it is used, in a single move

# change_background_color.rb

ChangeBackgroundColor.call('red')
Enter fullscreen mode Exit fullscreen mode

Incremental Release

The Incremental Release strategy states that you should deliver your code piece by piece to catch possible errors as soon as possible. For example, if we have the following code:

# change_background_color_1.rb

ChangeBackgroundColor1.call('blue')
Enter fullscreen mode Exit fullscreen mode

and

# change_background_color_2.rb

ChangeBackgroundColor2.call('blue')
Enter fullscreen mode Exit fullscreen mode

The Incremental Release Strategy suggests updating and deploying the first file first, and only after that, update and deploy the second one. For example, in the first deployment, we update the first file as follows:

# change_background_color_1.rb

ChangeBackgroundColor1.call('red')
Enter fullscreen mode Exit fullscreen mode

Then, in the second deployment, we update the second file as follows:

# change_background_color_2.rb

ChangeBackgroundColor2.call('red')
Enter fullscreen mode Exit fullscreen mode

So, when we have multiple changes, they should be split and gradually updated and deployed.

Feature Flags/Toggles

This is the first strategy where Feature Flags fit perfectly. In this strategy, we choose the behavior depending on whether the feature flag is enabled or not:

# change_background_color.rb

if Flipper.enabled? :red_background_color
  ChangeBackgroundColor.call('red')
else
  ChangeBackgroundColor.call('blue')
end
Enter fullscreen mode Exit fullscreen mode

It allows us to disable the feature if something goes wrong.

Canary Release

The Canary Release Strategy allows us to enable a feature for specific percentages of users. This enables us to perform slow rollouts. For example, if we want to enable the feature only for one user, we can do the following:

user = User.first

Flipper.enabled?(:red_background_color)
# => false
Flipper.enabled?(:red_background_color, user)
# => false

Flipper.enable(:red_background_color, user)

Flipper.enabled?(:red_background_color)
# => false

Flipper.enabled?(:red_background_color, user)
# => true
Enter fullscreen mode Exit fullscreen mode

But if we want to enable the :red_background_color feature for only 25% of users, we need to do the following:

Flipper.enable_percentage_of_actors(:red_background_color, 25)

user = User.first

Flipper.enabled?(:red_background_color, user)

# => true/false
Enter fullscreen mode Exit fullscreen mode

Please note that you can read more about how the algorithm actually works here.

Phased Rollout

The Phased Rollout Strategy allows us to enable a feature for specific groups, such as by country, role, or status etc. To enable this feature, we need to define the criteria or conditions for determining when the enabling should occur:

user = User.first 
# => <User id: 1, role: "admin">

Flipper.register(:admins) do |actor, context|
  actor.respond_to?(:role) && actor.role == 'admin'
end

Flipper.enable_group(:red_background_color, :admins)

Flipper.enabled?(:red_background_color, user)
# true
Enter fullscreen mode Exit fullscreen mode

Let's consider another example where we want to enable the feature for all users from the USA. It would look like this:

user = User.first 
# => <User id: 1, country: "USA">

Flipper.register(:from_usa) do |actor, context|
  actor.respond_to?(:country) && actor.country == 'USA'
end

Flipper.enable_group(:red_background_color, :from_usa)

Flipper.enabled?(:red_background_color, user)
# => true
Enter fullscreen mode Exit fullscreen mode

However, we have one problem. Where should we store Flipper.register(:admins)? Since we don't save the value of the block in the database, this block should be placed somewhere at the configuration level. Let's add it to the initializer:

# config/initializers/flipper.rb

# frozen_string_literal: true

require 'flipper'

Rails.application.reloader.to_prepare do
  Flipper.register(:admins) do |actor, context|
    actor.respond_to?(:role) && actor.role == 'admin'
  end
end
Enter fullscreen mode Exit fullscreen mode

That's it.

Fault Tolerance

In this strategy, we run the new version of the code, but if something unexpected happens, we fallback to the previous version. It can be implemented as follows:

begin
  ChangeBackgroundColor.call('red')
rescue UnexpectedError
  ChangeBackgroundColor.call('blue')
end
Enter fullscreen mode Exit fullscreen mode

Improvements

Our solution is not perfect; there is still some room for improvement. Let's explore how we can enhance the following aspects:

  • Removing the feature flag
  • Optimizing requests using caching
  • Providing a user interface (UI)
  • Adding an API
  • Adding validations for Feature Flags
  • Including information about the Feature Flags
  • Improving Spec Performance

Removing the feature flag

It's important to remember that every feature flag is a temporary solution for safe rollouts, and sooner or later they should be removed from the codebase. How can we do it? To remove a Feature Flag, we need to follow these steps:

Flipper.remove(:red_background_color)
# => true
Enter fullscreen mode Exit fullscreen mode

Optimizing requests using caching

To optimize the retrieval of feature flag information and avoid unnecessary database calls, Flipper provides the option to enable caching. To implement caching, you need to install the flipper-active_support_cache_store gem by adding it to your Gemfile:

# Gemfile

gem 'flipper-active_support_cache_store'

Enter fullscreen mode Exit fullscreen mode

After adding the gem, you need to update the configuration file as follows:

# config/initializers/flipper.rb

# frozen_string_literal: true

require 'flipper/adapters/active_record'
require 'flipper/adapters/active_support_cache_store'

Rails.application.reloader.to_prepare do
  Flipper.configure do |config|
    config.adapter do
      Flipper::Adapters::ActiveSupportCacheStore.new(
        Flipper::Adapters::ActiveRecord.new,
        ActiveSupport::Cache::MemoryStore.new,
        expires_in: 5.minutes
      )
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

With this configuration in place, you can now check if a feature is enabled without triggering a database call for the next 5 minutes:

# rails console

Flipper.enabled?(:red_background_color)
# triggers a database call

Flipper.enabled?(:red_background_color)
# does not trigger a database call
Enter fullscreen mode Exit fullscreen mode

Providing a user interface (UI)

To add a user interface (UI) for managing feature flags, you can follow these steps:

  1. Add the flipper-ui gem to your application's Gemfile:
# Gemfile

gem 'flipper-ui'
Enter fullscreen mode Exit fullscreen mode
  1. Execute the bundle command to install the gem:
bundle
Enter fullscreen mode Exit fullscreen mode
  1. Update your config/routes.rb file to mount the Flipper UI:
# config/routes.rb

YourRailsApp::Application.routes.draw do
  mount Flipper::UI.app(Flipper) => '/flipper'
end
Enter fullscreen mode Exit fullscreen mode

After updating the routes, the new UI will be available at http://localhost:3000/flipper (assuming your application is running on localhost and port 3000). You can find more information about using the Flipper UI here.

Adding an API

What if you want to expose the list of feature flags as an API? This can be useful when your client and API are separated, making it easier to retrieve the data. To add the Flipper API to your project, follow these steps:

First, install the flipper-api gem by adding it to your Gemfile:

# Gemfile

gem 'flipper-api'
Enter fullscreen mode Exit fullscreen mode

Next, execute the bundle command to install the gem:

bundle 
Enter fullscreen mode Exit fullscreen mode

Update your config/routes.rb file to include the Flipper API:

# config/routes.rb

YourRailsApp::Application.routes.draw do
  mount Flipper::Api.app(Flipper) => '/flipper/api'
end

Enter fullscreen mode Exit fullscreen mode

With this configuration, you can make a GET request to http://localhost:3000/flipper/api/features to retrieve the list of feature flags. The response will be in JSON format, as shown below:

{
  "features":[
    {
      "key":"red_background_color",
      "state":"on",
      "gates":[
        {
          "key":"boolean",
          "value":"true",
          "name":"boolean"
        },
        {
          "key":"actors",
          "value":[

          ],
          "name":"actor"
        },
        {
          "key":"percentage_of_actors",
          "value":null,
          "name":"percentage_of_actors"
        },
        {
          "key":"percentage_of_time",
          "value":null,
          "name":"percentage_of_time"
        },
        {
          "key":"groups",
          "value":[

          ],
          "name":"group"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

For more details on adding an API, refer to the Flipper documentation here

Adding validations for Feature Flags

As you may have noticed, our current implementation allows adding flags with any names and removing flags without restrictions. However, this approach is not reliable, as someone could make a typo and mistakenly enable/disable the wrong flag or accidentally remove a flag that is actively being used in production. Let's explore a possible solution to prevent such issues.

First, we can create a value object to keep track of all available feature flags. This will help us ensure that only valid flags are used. Let's create the FeatureFlag value object:

# app/value_objects/feature_flag.rb

# frozen_string_literal: true

class FeatureFlag

  class << self

    def all
      [
        'search',
        'red_background_color'
      ]
    end

    def supported?(flag_name)
      all.include?(flag_name.to_s)
    end

  end

end
Enter fullscreen mode Exit fullscreen mode

This value object provides a list of all available feature flags and allows us to check if a specific flag is supported:

FeatureFlag.all
# => ['search', 'red_background_color']
FeatureFlag.supported?(:red_background_color)
# => true
Enter fullscreen mode Exit fullscreen mode

Next, we can create a custom adapter that will enforce the availability of feature flags before enabling or adding them. The adapter can be implemented as follows:

# lib/feature_flags/adapters/active_record_based.rb

# frozen_string_literal: true

require 'flipper'

module FeatureFlags
  module Adapters
    class ActiveRecordBased < Flipper::Adapters::ActiveRecord

      def add(feature)
        return false unless supported_feature_flag?(feature.name)

        super
      end

      def enable(feature, gate, thing)
        return false unless supported_feature_flag?(feature.name)

        super
      end

      def remove(feature)
        return false if supported_feature_flag?(feature.name)

        super
      end

      private

      def supported_feature_flag?(feature)
        FeatureFlag.supported?(feature)
      end

    end
  end
end
Enter fullscreen mode Exit fullscreen mode

In this adapter, we check if the given feature flag is supported before performing actions such as adding, enabling, or removing it.

To include this custom adapter in the Flipper configuration, we can update the initializer file:

# config/initializers/flipper.rb

# frozen_string_literal: true

require 'flipper/adapters/active_record'

Rails.application.reloader.to_prepare do
  Flipper.configure do |config|
    config.adapter { FeatureFlags::Adapters::ActiveRecordBased.new }
  end
end
Enter fullscreen mode Exit fullscreen mode

Now, with these validations in place, the restrictions prevent performing actions with unsupported flags:

Flipper.enable(:not_supported)
# => false
Flipper.enable(:red_background_color)
# => true
Flipper.remove(:red_background_color)
# => false
Enter fullscreen mode Exit fullscreen mode

Including information about the Feature Flags

As we mentioned earlier, feature flags should eventually be removed. It would be beneficial to have more information about each flag beyond just the name. Let's add additional fields to the feature flag value object, such as:

  • Description
  • Expected expiration date
  • Owner

To achieve this, we can update the FeatureFlag value object:

# frozen_string_literal: true

class FeatureFlag

  class << self

    def all
      [
        {
          name: 'search',
          description: 'Search description',
          expected_expiration_date: 2024-01-01,
          owner: 'Backend team'
        },
        {
          name: 'red_background_color',
          description: 'Red background color description',
          expected_expiration_date: 2024-01-02,
          owner: 'Frontend team'
        }
      ]
    end

    def supported?(flag_name)
      all.map { _1[:name] }.include?(flag_name.to_s)
    end

  end

end
Enter fullscreen mode Exit fullscreen mode

Now, each feature flag includes additional information such as description, expected expiration date, and owner.

Note: You can also consider defining constants in a YAML file to simplify the interface and keep the information organized.

Improving Spec Performance

To improve the performance of your tests, it is recommended to avoid unnecessary database hits when working with feature flags. You can achieve this by replacing the Active Record adapter with the Memory Adapter for your test environment.

Here's what you need to do:

Update your config/initializers/flipper.rb file as follows:

# config/initializers/flipper.rb

# frozen_string_literal: true

require 'flipper/adapters/active_record'

Rails.application.reloader.to_prepare do
  Flipper.configure do |config|
    config.adapter do
      Rails.env.test? ? Flipper::Adapters::Memory.new : Flipper::Adapters::ActiveRecord.new
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

With this configuration, the Memory Adapter will be used for the test environment, while the Active Record Adapter will be used for other environments.

Now, when you run rails console in the test environment (rails c -e test), you can verify that the database is not being triggered when working with feature flags:

# rails c -e t

Flipper.enable(:red_background_color)
Flipper.enable?(:red_background_color)
Enter fullscreen mode Exit fullscreen mode

This change will help improve the performance of your test suite by eliminating unnecessary database hits.

Conclusion

In this article, we've been diving deep into the world of feature flags. We explored the benefits of using feature flags, including risk mitigation, A/B testing, controlled rollouts, and faster development cycles.

We discussed different feature release strategies, such as the Big Bang Release, Incremental Release, Feature Flags/Toggles, Canary Release, Phased Rollout, and Fault Tolerance. Each strategy offers a unique approach to releasing new features or updates, providing flexibility and control over the rollout process.

To enhance our feature flag implementation, we made several improvements. We added validations to ensure that only supported flags can be enabled or added, preventing potential errors. We integrated caching to optimize performance by reducing unnecessary database hits. Additionally, we explored the options of providing a user interface and adding an API to expose feature flag information to clients.

Top comments (2)

Collapse
 
katafrakt profile image
Paweł Świątkowski

Nice article! I especially like how you listed advantages vs problems - and I think that list is pretty exhaustive.

My personal take about FFs is that in most projects I just wished we rolled them out ourselves instead of using ready-made solution. I know it sounds like reinventing the wheel, but truth is that every organization has its specifics and finds itself working around limitation of off-the-shelf solutions quite soon. As an example, in my previous company we needed to roll out country-by-country, which was not supported ootb. In current, we want to flag not by the users making the request, but by the user creating the resource, with which the actual user interacts. Also not supported, and we need to do some weird stuff with user tags to achieve that.

Collapse
 
vladhilko profile image
Vlad Hilko

Thank you! Yeah, I agree, sometimes ready-made solutions don't have what we need. It would be interesting to hear about the solutions you devised and how they have been implemented