Build a minimal feature flags manager in under an hour
When you deploy your code continuously, feature flags (or feature toggles) are a neat way to hide in-progress features from your users. In the past, I’ve written about my process for releasing large features. But after using feature flags in the last six months, I’ve come to like them better than my former process.
Still, hiding behavior behind if ENV['MY-FEATURE'].present?
-type conditionals bothers me.
First, I don’t think the syntax is very legible. Second, I know it’s hard to maintain a naming convention across a team. You’ll soon end up with endless variations: ENV['MY-FEATURE]
, ENV[FF-MY-FEATURE-NAME]
, ENV['TOGGLE-FEATURE']
, etc… These discrepancies make it hard to keep track of all your feature flags.
Sure, you could use a gem or a third-party service. But for those who just need an on-and-off system, here’s a minimal feature flag manager that’ll take you less than an hour to build. It’s a plain Ruby object but its configuration leans on Ruby on Rails. I’m sure a Ruby-only implementation wouldn’t be too hard to whip up, though.
Expected outcome
Instead of if ENV['EDITORIAL_FEED_FEATURE'].present?
, I’d like to see this syntax:
While looking at it, a few ideas come to mind:
- My
Features
class will need access to all available feature flags and check if the flag passed as argument is present and enabled. - All my feature flags will need to follow the same naming strategy.
- Since I’ll probably still use environment variables at some level of my stack, I’ll need somewhere to centralize all those variables related to feature flags.
- My method
enabled?
should return eithertrue
orfalse
.
A basic implementation of my Features
class could be this:
A shameless green version
What behavior should we expect from our Features
class?
If you pass a feature’s name to Features.enabled?
, the method should:
- Check if the feature’s name is present in a list of feature flag.
- Check if the feature flag is either
true
orfalse
. - Return a boolean.
Let’s draft our first test.
A minimal implementation could be:
This code is problematic on several levels. Our test is green, though. If you were to put this code into the world right now, it’ll work. I also like that all feature flags are neatly organized in one place.
Some of the problems we can guess:
- Every time we need to switch a feature flag from
false
totrue
, we’ll have to push changes to production. - Having a constant in the class might prove inefficient when the number of flags grows.
- We’re not relying on environment variables like we originally intended to.
Let’s try and tackle the latter first.
Use environment variables
The smallest step we can take is to replace hardcoded values with environment variables.
With this code, our test still passes but not for the right reasons.
The application evaluates the presence of environment variables at runtime. If one misses in ENV
, the variable will default to false
instead of crashing (thanks to fetch
accepting a default value).
So our test does not really evaluate the presence and setting of our feature flags. It only tests if the return value is truthy or falsy. And it is, since fetch
defaults to false
every time. We could mock the value ENV
returns, but it’ll couple our tests too tightly to our testing environment. This smells like a bad idea.
Our tests could be better if we changed the expectation from .to be_in([true, false])
to a more constrained .to be(true)
. This would make sure that the tests don’t return a false positive.
Another thing I don’t like is passing variables - which are variable by definition - to a constant? Meh.
I’d rather group these variables into a dedicated file. If we could load all these feature flags’ variables at boot time, we’d save up some time when our application needs to evaluate those flags.
Move your environment variables to a single file and load them at boot time
One way to group your feature flags in the standard Rails configuration. And it’s simpler than it sounds.
You can create a custom accessor in your Rails configuration that leans on a single YAML file.
First, go to your application.rb
. At the end of the Application
class, add your own configuration.
What’s happening here? Now, your Rails::Application::Configuration
class has a features
accessor that you can call with Rails.config.features
.
Rails.config.features
will return an ActiveSupport::OrderedOptions
that inherits from Hash
and provides a dynamic accessor method.
Your application will now look for a YAML
file named features.yml
.
In your config
directory, create a features.yml
file. The first key represents the environment where your variables will apply. In our example, I’ve used shared
which means that all environments will share the information.
We usually store strings in YAML
files, but it’s possible to execute Ruby code with the help of the <%= =>
syntax.
Now, we have a single file where all our feature flags and their respective environment variables will be neatly gathered. I used to be a librarian, I love things arranged neatly.
Access our feature flags through Rails configuration
Now, a single file centralize all our feature flag keys and their respective environment variables. I used to be a librarian. I love things arranged neatly.
It’s time to get back to our Features
class.
We don’t need our constant anymore. But we need to pull the configuration in its place.
Let’s whip up a second test.
Let’s try to code our method and make the test green.
Easy right! The test passes. On the other hand, we’re creating a dependency between our Features
class and the Rails configuration.
Now that we’ve made the change easy, it’s just a matter of changing FEATURES
by configuration
in our enabled?
method.
The full test suite is:
Okay, so why does my second test throw a failure?
Well, we created a dependency between Features.configuration
and the Rails configuration. And that dependency needs to be mocked!
This’ll take care of the dependency. Now, my tests pass.
Check for different return scenarii
What happens if the feature we’d like to check is not in our list? configuration[feature.to_sym]
will return nil
. Let’s update our tests accordingly.
My third test passes. But by convention, enabled?
should return a boolean, not nil
. So we need to enforce its return type.
Enforce a boolean return type
First, let’s update our tests.
There are several ways of doing this. Initially, I’d decided to modify TrueClass
, FalseClass
, and NilClass
to accept a to_boolean
method. Several readers pointed out it was a tad overkill.
Use hashes capabilities
Since .configuration
return an object inheriting from Hash
, we can use .fetch
on it.
What happens is if our .configuration
contains feature
we’re passing along, .enabled?
will return the boolean stored in the YAML. If feature
is not present, .fetch
will default to false
.
Use .present?
Another suggestion was using .present?
on my configuration hash.
The return results’ strategy works as in the previous example.
Use the double bang
A last suggestion was using !!
a.k.a the double bang. This would look like this:
The rationale behind it is:
If you negate something, that forces a boolean context. Of course, it also negates it. If you double-negate it, it forces the boolean context, but returns the proper boolean value.
And voilà! Your feature flags manager is ready. Now, you can safely wrap your features with Features.enabled? :editorial_feed
conditionals!
Hope you liked this code-along as much as I did!
Cheers,
Rémi - @remi@ruby.social
PS: Many thanks to @NotGrm, @sunfox, @_swanson, and Kaloyan for their suggestions!
PPS: John Nunemaker wrote a Rebuttal and Addendum to this post. John - whose blog I love - makes some great suggestions to better the feature flags manager. I like the idea of switching features during runtime without restarting your dynos. Obviously, as the creator of Flipper - a gem dedicated to flipping features - he’s created something much more comprehensive for people who need more control over their feature toggles.