2021-09-09 6min read

Replacing long-running branches with feature flags

Git branches are often used to write features that take a long time to write. I’m starting to think that they may not be the ideal solution.

Screenshot of a Git merge conflict on GitHub
Constantly working with Git merge conflicts is never fun.

Issues with Git branches

A branch might be worked on for weeks then finally merged to the main branch when it’s done. Almost every team I’ve worked on runs into the same issues with long-running branches:

💣 They get out-of-sync — Branches will require regularly merging in changes from main. Devs tend to avoid this chore, making things worse.

💣 They’re impossible to review — Long-running branches often live as pull requests with thousands of lines.

💣 They’re difficult to revert — If any issues arise after merging into production, features may need to be “undeployed.” This is easy with git revert until more code piles up to make it impractical.

Alternative: feature flags

What if WIP features can be merged into production? Maybe it can. Feature code in the main branch doesn't have to be made available in production. Blocks of code can always be turned off with conditional statements.
if (process.env.NODE_ENV === 'production') {
  // ...
}

These conditionals are often called feature flags or feature toggles. I’ll list down a few ways I know of implementing feature flags, starting with the simplest (level 0) to the more advanced (level 7).


Level 0: Hiding UI in production

A feature's UI can be disabled in production with an if condition. This would allow the app to be deployed even if some features aren't complete yet.
example.tsx

const isProduction =
  process.env.NODE_ENV === 'production'

<div>
  {isProduction ? null : <button>Show lyrics</button>}
</div>

example.erb

<% if Rails.env.production? %>
  <button>Show lyrics</button>
<% end %>

Example: this JavaScript snippet hides a button in production mode based on NODE_ENV (Node.js) or RAILS_ENV (Ruby on Rails).

Caveats to note

💣 Testing can be difficult — this makes the new features available in tests, and toggling these in unit tests means having to hijack environment variables somehow.

💣 Features also show up in dev mode — config-based checks are often preferred over environment checks for this reason.

✨ Next, let’s organise mutiple flags with configuration.


Level 1: Per-feature configuration

Having one switch per feature can be more flexible, not to mention more organised. Rather than relying on NODE_ENV, each environment can have a list of flags that are enabled for it.

Common conventions include environment variables (shown below) or YAML configuration files.

.env
LYRICS_VIEW_ENABLED=0
TRENDING_PAGE_ENABLED=0
.env.development
LYRICS_VIEW_ENABLED=1
TRENDING_PAGE_ENABLED=1
Different environments can each have configuration that determines what features are on.

Caveats to note

💣 URL’s can still be guessed — while it’s often enough to hide links to new features, some new URL routes may also need to be hidden.

✨ Next, let’s secure the app’s URL routes.


Level 2: Suppressing the routes

URL routes can be disabled in production in the same way. While hiding links will make the feature invisible, that doesn't prevent users from guessing the URL of new features.
routes.js
const isProduction =
  process.env.NODE_ENV === 'production'

if (!isProduction) {
  router.get('/browse/trending', browseByTrending)
}

A JavaScript example of how URL routes can be turned off based on environment variables.

Caveats to note

💣 Features aren’t available in production — This is often what we want at first, but at some point it would be nice for the internal team to try the feature out in production.

✨ Next, let’s try enabling the feature for a limited set of users.


Level 3: User flags

Features can be limited to certain users such as "admin" users. This allows the team to test out new features in production.
const user = getCurrentUser()

<% if (user.admin) { %>
  <a href='/trending'>View trending albums</a>
<% } %>
In this example, the "Apply now" button is only shown to admin users.

Caveats to note

💣 User accounts are required — This makes this method not suitable for features not requiring users to sign in, or sites without authentication.

💣 Requires database storage — While it’s easy for some apps to store metadata for a user (eg, an admin flag), this might not always be the case.

✨ Next, let’s look at a possible solution that might remove the need for state storage.


Level 4: Cookies

Features can be restricted in production using special cookies. Unlike the user-based approach, this allows testing of features that don't require signing in.
<% if (req.cookies['trending-enabled'] === '1') { %>
  <a href='/trending'>View trending albums</a>
<% } %>
This example shows a link only for users with a certain cookie.

Caveats to note

💣 May be difficult to enable — Developers may know their way around adding new cookies, but non-technical people may need something more user-friendly.

💣 Can be prone to tampering — Some users will be able enable features if the cookie names leak out.

✨ Next, let’s try to make this a bit more user-friendly.


Level 5: UI for setting cookies

A simple page can be used to list all cookie-based flags and provide a UI to enable and disable them.

Major browsers today have a secret page to allow enabling experimental features. Something like this can also be done for web apps, possibly hidden by authentication.

Screenshot of the Experiments page on Microsoft Edge
Microsoft Edge features an about:flags page for experimental features.

Caveats to note

💣 Can be prone to tampering — clever users will be able enable features if the cookie names leak out.

✨ Next, let’s try to prevent unauthorised users from enabling feature flags.


Level 6: Signed cookies

Cookie-based feature flags can be cryptographically-signed to avoid tampering. This is done in a number of ways depending on the framework being used. Rails's cookies.signed appends the payload with an HMAC signature, similar to JWT tokens.

Level 7: Managed feature flag service

A number of third-party providers offer feature flag management. This brings features like to allow rolling out features by location, by AB split testing, and more. These can be used to make WIP features only available to internal team users, too.