Boring Rails

Feature Flags: The stupid simple way to de-stress production releases

Apr 12th, 2020 9 min read

“So, uh, is it okay to deploy this?”
“Wait, hold on, that’s not ready to go-live!”
“Weird, they shouldn’t be able to see that yet…“
“Oh shit, the deploy failed, what do I do?”
“Update for today: I’ve been merging in changes to my feature branch”

Feature flags (or feature toggles) are a technique for incrementally rolling out functionality in an application. Work that is not completely ready to go live can be released in chunks while being hidden by a flag. These flags are usually configured per-environment so that you can, for instance, enable a feature on a staging site before turning it on in production.

Feature Toggles by Martin Fowler is the canonical article on this practice and outlines a full taxonomy of feature toggles.

Toggles are broken down into four categories:

  • Release toggles: allow in-progress code to be shipped to production
  • Experiment toggles: support A/B testing multiple code-paths
  • Ops toggles: panic button / circuit breakers
  • Permission toggles: changing product features (premium, early access, etc)

Martin Fowler: Feature Toggle Chart

Martin Fowler: Feature Toggle Chart

Each type of toggle has different needs and properties. For most small to medium sized apps, one type stands above the others in terms of value: release toggles.

Generally, long running branches are a self-imposed disaster for teams that want to ship fast. The stock answer to “how do we avoid long running branches” is to do continuous integration – and feature flags are often the missing conceptual link needed to bridge the textbook concept of CI with the practicality of “okay, how do we, you know, actually do that?”.

While the other types of toggles are more context specific, every app can benefit from simple release toggles.

How should we use feature flags?

Factors that determine if feature flags are a joy or a rats nest of complexity:

  • Quantity: how many flags do you have running at a time?
  • Duration: how long are these flags hanging around?
  • Urgency: how quickly would you need to turn a flag on/off?
  • Depth: at what application level does the flag need to exist?
  • Risk: how thoroughly do you need to flag a feature?

These factors are intertwined and have cascading effects.

If you only have features flagged for a few days, you may be willing to accept more risk in partially exposing a feature.

If your deploy process is currently slow, you’ll want to pick a solution that allows changing flags at runtime.

If you have one or two flags at a time, are working on new features that can be hidden at a high level, and someone manually guessing the URL of unreleased features isn’t a problem, you can literally get away with glorified if statements.

It’s always best to start small. As Sandi Metz would say: “The future is uncertain and you will never know less than you know right now”.

Keep your options open and do the simplest thing that could possibly work and build from there (if you ever need to).

How should we implement feature flags?

There are two approaches to implement feature flags: basic conditionals and “feature manager” libraries.

Basic Conditionals

Feature flags are, at their core, an if statement.

You can wrap up a very simple class to make the developer experience better and to keep your features organized. For getting started, don’t bother with any gems or external tools. Simply add a class like this to your Rails project.

class Feature
  def self.enabled?(feature_name)
    case feature_name.to_sym
    when :meeting_transcripts
      !Rails.env.production?
    else
      true
    end
  end
end

- if Feature.enabled?(:meeting_transcripts)
  # Do your thing

Take advantage of established patterns like Rails.env or Current.user to write simple conditionals to return true or false for any given feature. Since you completely own this code, you can adapt it however you want.

This seems too primitive for the real-world, but even this kind of stupid simple “disabled in production” flag is often the only kind of logic you’ll need.

Pushing the Limits

If you do want to further push the boundaries, you can try some of these clever hacks:

Current.user.admin? || !Rails.env.production - on for everyone in test, but only admins in production

Current.account.early_access_enabled? - add fields to domain models for “opt-in” groups

Current.user.email.contains("s") - roughly split a group in half using this hack from email marketing

Current.user.id % 100 < 10 - roll out a feature to roughly 10% of users (it’s not a proper random sampling but you’re not actually going to do real statistical analysis anyways)

These advanced conditionals are clever, but clever is not inherently good. Prefer more boring per-environment flags whenever possible.

Remember that we are primarily using these flags as release toggles. If you’re looking to do actual data-driven analysis (which requires more statistical rigor than reading a blog post…) or expect the logic to be a permanent part of your application, you should look elsewhere.

Feature management tooling

The other route teams take is to manage flags at run-time with a gem like flipper or use a hosted service like Flipper Cloud.

Sadly, no product manager has ever been able to understand this…

There are some other gems (rollout, flipflop, etc), but conceptually, they all end up using Redis as a key-value store, apply a bit of logic (percentage calculations, groupings, etc), and return true/false.

In exchange for the additional machinery, you gain the ability to turn features on and off while the app is running or even have non-developers manage the roll-out.

In reality, I’ve found that I rarely needed either of these features. Heavier feature flagging tools are both overkill for a simple cases (0-3 flags) and hard to manage if you have 5+ flags (which features are on? how do they interact? why is this off on staging, but on in production?).

With flipper, the deployment process becomes decoupled from your normal development workflow. Depending on your context, this may be a positive or a negative.

The stupid-simple Feature class requires a code commit to change, which is slightly annoying but then hands-off. Using flipper requires you to pull up the admin page in the right environment and click some buttons, or remote in and remember to flip some bits in a rails console.

Where should you put feature flags?

Whenever possible, flag things at the “edges” of features. If you’re adding a new section, hide it in the navigation menu. If you’re building a new action on an existing page, hide the button.

The fewer flags you need to put in your codebase at a given time, the easier it will be to manage.

If you can get away with it, simply hide things at the view level. For 99% of users, if something doesn’t exist in the UI, it doesn’t exist in the app. Hell, it’s often hard enough to get users to notice new features that aren’t hidden!

A “loosely” flagged feature will allow you to manually key in the URL if you want to run a quick test in an environment that isn’t officially enabled.

But if you are changing some internal logic, or if there are risks with allowing users to potentially access features that in still underdevelopment, you’ll need to move your flags closer to the “core” of the system.

Adding flags deep in a controller, model, or service is fine, but should be used only when necessary. Remember the same “keep it stupid simple” approach even as you move farther from the edges.

For instance, if you want to 404 all endpoints for a new feature, you can drop in this 20-line concern to avoid sprinkling conditions everywhere.

module FeatureFlaggableController
  extend ActiveSupport::Concern

  class_methods do
    attr_reader :feature_flaggable_name

    def feature_flag(feature_name)
      @feature_flaggable_name = feature_name
    end
  end

  included do
    prepend_before_action :enforce_feature_flag!

    private

    def enforce_feature_flag!
      if self.class.feature_flaggable_name.nil?
        raise ArgumentError.new("No feature flag specified! \n\nPlease call `feature_flag(:some_flag)` in #{self.class.name}.")
      end

      unless Feature.enabled?(self.class.feature_flaggable_name)
        raise AbstractController::ActionNotFound
      end
    end
  end
end
class PostsController < ApplicationController
  include FeatureFlaggableController
  feature_flag(:posts)

  # ...
end

How do you roll-out features with this basic approach?

Rolling out features can be stressful, but by using feature flags in an incremental way, you can make your releases calmer.

I generally try to follow these steps:

  • Add a feature flag when starting work on a big feature (default to “on” in non-production environments)
  • As functionality is ready, merge frequently and deploy behind the flag
  • With the flag still on, deploy finished feature to production and test / check things out
  • Update the feature logic to always return true, deploy and observe
  • If everything looks good, delete the feature logic completely

These are not hard and fast rules, but the key concepts are to avoid long-running branches and high pressure deploys where a ton of code is immediately going live in production for the first time.

It may seem like it would slow you down, but for most features it’s only adding a few minutes of extra time. It only takes one production rollout screw up to burn you before you’ll start using release flags for any medium or large features.

Wrap It Up

Shipping fast means deploying frequently and hiding in-progress work behind feature flags lets you safely rollout changes in a more controlled way. No more big “hope we don’t run into problems” deploys that create high stress situations. No more long running feature branches.

Feature flags bridge the gap between the abstract concept of continuous delivery and tactical release of features. While there are many kinds of feature toggles, simple release flags provide the best benefit-to-cost ratio.

You can achieve calmer deployments by making feature roll-outs a trivial event. Start small and use a boring Feature class before investing in fancy, enterprise-grade feature management tooling.

Was this article valuable? Subscribe to the low-volume, high-signal newsletter. No spam. All killer, no filler.