Simple Feature Flags in Ruby
Published on September 1, 2022Deploying new code to production can be nerve-wracking. Things can break, unexpected behaviours can show up, all kinds of things can go sideways. It’s extremely important to be able to quickly revert changes, in the case that things go wrong unexpectedly. There are a bunch of different ways to approach this, but here I’ll be talking about Feature Flags.
NOTE There is some preamble here before we get to the code snippets. If you'd like to skip directly to the full code, click here, or scroll to the bottom!
First, what’s a feature flag? I’m sure there’s a proper technical definition, but as far as I’m concerned, a feature flag (sometimes called a feature toggle), is a mechanism to enable or disable features on the fly without deploying anything new or changing any code. Ideally, you should be able to enable or disable a feature instantaneously. Depending on the particular implementation, it may not be quite that fast, but within seconds-to-minutes, rather than minutes-to-hours.
Another important use-case for feature flags is to regulate which of your users have access to a particular feature. This can be useful for things like beta-testing a new integration, A/B testing a new layout, or slowly opening the floodgates to ensure that a new code path can hold up to production traffic.
Now, you may be thinking, isn’t this a solved problem? Aren’t there like, a million, libraries in every language that will implement this for you? And to that I say, yes, yes there are. Some very smart people have thought about this problem, and put good work into solving it. Libraries like Flipper for Ruby, Fun With Flags for Elixir, and even full-on SAAS products like LaunchDarkly can help you implement sophisticated, robust feature flags for your application. But! As we all know, sometimes, adding a library or integrating with a full-on external provider is WAY overkill for your needs. And that's why we're here today.
In my experience, the simplest way to manage feature flags in a
small-to-medium-sized application, especially with a small (or one-person) dev
team, is to use environment variables. Check if the variable is enabled, and use
that to determine whether or not to show the feature. Simple enough. However,
this barebones approach quickly runs into some limitations. What if, for
example, you need to check for the variable in multiple places? What if you need
to send the feature flag status to a client-side application? What if a flag has
more complex states than just on
and off
?
When I came across this problem, I decided to put together a slightly more sophisticated system to streamline the process of checking the status of a flag, and for distributing that knowledge throughtout an application.
At it’s core, my solution involved a class for each flag. Something like this:
class NewLayoutFeatureFlag
class << self
def value
ENV["NEW_LAYOUT_ENABLED"]
end
def parse
return true if value == "true"
return value.split(",").map(&:to_i) if value&.match?(/[0-9,]+/)
false
end
def enabled_for_user?(user_id)
parsed = parse
return parsed if [true, false].include?(parsed)
parsed.include?(user_id)
end
end
end
In my use-case, I had a flag that could be in one of 3 states: on
, off
, or
enabled for a subset of users, identified by ID, which looks like this:
2,23,432
-- a comma-separated list of IDs for which to enable the flag. In
this particular case, the flag is stored in an environment variable, but that is
certainly not a requirement of this method.
For me, the key to this approach is the simplicity of it’s interface. Wherever I
am in my application, I can call NewLayoutFeatureFlag.enabled_for_user?
with
the user ID, and get back whether or not the flag is enabled for that user. The
Flag itself knows how to check whether a particular user should be able to use
a feature. This interface allows the flag code to be as simple or as complex as
it needs to be. It could do a DB lookup, it could store the state in Redis, or
perform some work to determine whether or not the flag should be enabled.
Having a simple interface like this allows you to use the flag in some really
nice patterns. Need to enforce the flag in a controller action? No problem,
stick it in a before_action
call, like this.
class FooController < ApplicationController
before_action :check_new_layout_feature_flag
...
private
def check_new_layout_feature_flag
unless NewLayoutFeatureFlag.enabled_for_user?(@user.id)
render json: {errors: "This feature is not enabled for this account"},
status: :unprocessable_entity
end
end
end
Need to share the state of the flags with other clients via an HTTP call? Simple. Something like this is a quick solution.
class FeatureFlagsController < ApplicationController
before_action :set_user
def show
flag_klass = flags[params[:flag]]
render json: {
params[:flag] => !!flag_klass&.enabled_for_user?(@user.id)
}, status: :ok
end
private
def flags
{
"new_layout_enabled" => NewLayoutFeatureFlag,
...
}
end
end
# and in config/routes.rb
get "/feature_flags/:flag" => "feature_flags#show"
This kind of architecture makes it simple to add new flags. You could even add a
FeatureFlagsController#index
action to return the status of all the flags
for a current user.
Another important feature of this feature flag design is that the default
position of all the flags is off
. I don't want any functionality to sneak
through the feature flags. If a user passes bad data, if something goes wrong
internally, I want the feature turned off. Of course, that logic may change
depending on the use case. But again, that speaks to the flexibility of this
approach. You could even integrate this inteface with a third-party like
LaunchDarkly or Flipper, if you decided you need to have a mix of in-house and
externally-managed feature flags.
While I’m sure this solution doesn’t cover all possible feature flag use-cases, I’ve found it’s a solid solution for my needs. It’s structured enough that I don’t need to think any further about how to check the status of a flag, but it’s flexible enough to allow me to use different types of flags for different use cases.
To wrap up, I’d like to thank YOU, the reader, for making it this far! It’s a bit more text than I’d envisioned, but I hope it has some value. If you loved the article, or hated it, or have any tips or corrections, please reach out to me at mike@mikebowman.dev! I’m also a freelance developer with lots of experience in Rails, Elixir/Phoenix, and React! If you like my work and could use some extra hands on your project, please don’t hesitate to reach out! I’m always open to new connections!
Full Code
# flag implementation
class NewLayoutFeatureFlag
class << self
def value
ENV["NEW_LAYOUT_ENABLED"]
end
def parse
return true if value == "true"
return value.split(",").map(&:to_i) if value&.match?(/[0-9,]+/)
false
end
def enabled_for_user?(user_id)
parsed = parse
return parsed if [true, false].include?(parsed)
parsed.include?(user_id)
end
end
end
# controller
class FeatureFlagsController < ApplicationController
before_action :set_user
def show
flag_klass = flags[params[:flag]]
render json: {
params[:flag] => !!flag_klass&.enabled_for_user?(@user.id)
}, status: :ok
end
private
def flags
{
"new_layout_enabled" => NewLayoutFeatureFlag
}
end
def set_user
# set user from params, session, etc.
@user = ...
end
end
# config/routes.rb
get "/feature_flags/:flag" => "feature_flags#show"