Feature flags: an alternative to branches

Can we do better than Gitflow? Maybe we don't need to keep branches around anymore.

Written by Rico Sta. Cruz
(@rstacruz) · 9 Sept 2021
Merge conflict on GitHub.com

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.

What’s wrong 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.

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).

example.js
if (process.env.NODE_ENV !== 'production') {
// ...
}
A simple conditional statement to only run code in development—a rudimentary form of feature flags.

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.

client_side_example.tsx
const isProduction = process.env.NODE_ENV === 'production'
function MyComponent() {
return (
<div>
{isProduction ? null : <button>Show lyrics</button>}
</div>
)
}
server_side_example.rb
<% if Rails.env.production? %>
<button>Show lyrics</button>
<% end %>
  • Pro: The feature works in development, but not in production. This means the feature can be merged into the main branch and deployed without affecting users.

  • Con: 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.

  • Con: Features also show up in dev mode. Config-based checks are often preferred over environment checks for this reason.

Next, let’s organise multiple flags with configuration.

👋
Hey! I write articles about web development and productivity. If you'd like to support me, subscribe to the email list so you don't miss out on updates.

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.
  • Pro: Easy to see what features are available, and they can be adjusted per feature (eg, to deploy one feature to production).

  • Con: URL’s can still be guessed. For example, a trending page may be available in /browse/trending which others can guess. While it’s often enough to hide links to new features, some new URL routes may also need to be hidden.

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.
  • Pro: Same pros as above (level 1) with none of its cons.

  • Con: 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: Restrict to admin users

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 "View trending albums" button is only shown to admin users.
  • Pro: If your app already has a functionality to discern admin users, this can be an easy win.

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

  • Con: 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.
  • Pro: Internal team members will be able to try out the feature in production before real users see it.

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

  • Con: 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.

About flags
Microsoft Edge features an about:flags page for experimental features.
  • Pro: Internal team members will be able to try out the feature in production before real users see it.

  • Con: 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.

Conclusion

Git branches may not be the ideal solution for long-running features. Branches often become out-of-sync, are difficult to review, and can be challenging to revert if issues arise. An alternative solution is to use feature flags or feature toggles. By using conditional statements, features can be merged into the main branch without being made available in production.

Have you used feature flags below? What was your experience like? Let me know in the comments below!

Written by Rico Sta. Cruz

I am a web developer helping make the world a better place through JavaScript, Ruby, and UI design. I write articles like these often. If you'd like to stay in touch, subscribe to my list.

Comments

More articles

← More articles