An Idiosyncratic Blog

The Build Series: E6 — Fun with Flags

Published on
·
20 minutes read

There are times when you want to deploy a feature to production, but only activate it for a certain set of users. Or perhaps you want to ship multiple features without waiting for an "official" release, but activate the features once it's green-lit by the PMs or Sales. Or you want to create a time based release with a kill-switch. All of these are possible with feature flags.

Feature flags, or toggles, as described by Martin Fowler are a “powerful technique, allowing teams to modify system behavior without changing code.”martinfowler.com

At it's core, Feature Flags are just a way to define a set of conditions that must be met in order for a feature to be active. It can be achieved in a number of ways, but the simplest is by using a if statement in your code. It’s like a light switch. 💡

In this episode of The Build Series, we'll build a full-fledged feature flag system using Next.js.

Table of Contents

Defining a Flag

Feature Flags can be either static or dynamic.

  • Static flags are defined at compile time, and are activated by a logic block in code.
const showFeatureX = true
if (showFeatureX) doTheThing()
  • Dynamic flags are defined at runtime, and are activated by a series of algorithms which determine outcomes based on a combination of factors. These are usually driven with the help of an API or Backend system.
const showFeatureX = await fetchFlagStatus('featureX')
if (showFeatureX) doTheThing()

We can categorize Feature Flags as:

  • Release Flags: Allows to ship a feature to production ahead of release schedule, and activate it once it's green-lit by the PMs or Sales teams. It also allows shipping in-progress code which needs to be merged to the main branch but not yet released.
  • Experiment Flags: Allows to ship a feature to production, but activate it only for a certain set of users, by looking at either the cookies or location. These are used for A/B testing and gather data on how users are perceiving the feature.
  • Ops Flags: These are kill-switch flags that are used to turn on/off features in production without the need to perform a full deployment cycle.
  • Permission Flags: These are flags that are used to allow or disallow a feature based on a user’s permissions. It's useful in Admin apps, where you want to allow users to access certain features based on their permissions.

Once we know what type of flag we're dealing with, we can define it in our code. For the sake of simplicity, we'll define our flags with the following structure:

{
  name: "featureX",
  desc: "This is a description of the feature",
  type: "EXPERIMENT | RELEASE | OPS | PERMISSION",
  conditions: [{type: '', name: '', value: ''}],
}

Depending on use-case, we can define more metadata about the flag. For example, we can define a team name which is used to identify the team responsible for the maintenance of the flag.

Defining Conditions

Once we know what type of flag we're dealing with, we can define the conditions that must be met in order for the flag to be active. The conditions are defined in an array of objects. Each object has a type property, which defines the type of condition. The other properties are specific to the type of condition.

{
  "name": "featureX",
  "desc": "This is a description of the feature",
  "type": "EXPERIMENT",
  "conditions": [
    {
      "type": "COOKIES",
      "name": "featureX",
      "value": "BSE1191"
    },
    {
      "type": "HEADERS",
      "name": "x-geo-location",
      "value": "IN"
    }
  ]
}

The above example is a very trivial way to define the conditions. We use these conditions to determine if the flag is active. For instance, if the flag is an Experiment Flag, we can use the COOKIE condition to check if the user has a cookie with a specific value. If the cookie is present, the flag is active.

Defining Controls

Since we are using React, we will define a Control component which will be used to render the feature if it's active.

/**
 * A Feature Flag toggle that renders component based on whether or not flags are active.
 * @param flags a list of flags
 * @param strict true = should activate only when all flags are active. false = should activate if one or more flags are active
 * @returns component if flags are active, null otherwise
 */
const FeatureControl = ({ flags = [], strict = false, children }) => {
  const activeFlags = useFeatureFlags()
  const activate = strict
    ? flags.every((f) => activeFlags.includes(f))
    : flags.some((f) => activeFlags.includes(f))
  return activate ? children : null
}

FeatureControl component takes two props,

  • flags: an array of flags, which are required for the component to render
  • strict: a boolean indicating if we want to match all the flags, or just one or more.

useFeatureFlags() is a hook which will make an API call to the backend and get the list of flags which are active.

const useFeatureFlags = () => {
  const flags = await fetchFlagsFromBackend();
  return flags;
};

We can then use FeatureControl to toggle sections of the app based on whether a flag is active.

const App = () => {
  return (
    <div>
      <FeatureControl flags={['featureX']}>
        <FeatureXComponent />
      </FeatureControl>
      <FeatureControl flags={['featureY, featureZ']} strict>
        <FeatureYComponent />
        <FeatureZComponent />
      </FeatureControl>
    </div>
  )
}

FeatureXComponent is rendered, when featureX flag is active. FeatureYComponent and FeatureZComponent are rendered when both ["featureY, featureZ"] are active. If either of them are inactive, FeatureYComponent and FeatureZComponent will not be rendered.

This is how FeatureControl component looks in action:

Play with the return value of useFeatureFlags to see how it behaves.

Building the Application

Now that we have a fairly good idea of how to use Feature Flags, we can start building the application. We'll use Next.js as our framework of choice because it takes care of the routing and provides a nice API for us to use.

First step, create a Next.js boilerplate app and open it in your editor.

npx create-next-app fun-with-flags

Server Side

Next.js provides a feature, API routes, which can be used to build, you guessed it...API routes. Any file inside the folder pages/api is mapped to /api/* and will be treated as an API endpoint instead of a page. Remember in our earlier example, how the useEffect was calling an API to get the list of active flags? That's exactly what we are doing here.

You don't have to use the API routes if you already have a custom server implementation.

Create a pages/api/feature-flags.js file which will be our endpoint (/api/feature-flags) to get the list of active flags. (Feel free to delete any file created as part of the boilerplate.)

import fs from 'fs-extra'

const FILE_PATH = `./data/flags.json`

const readFile = () => fs.readJSON(FILE_PATH)

const assertConditions = (flag, request) => {
  const { cookies, headers } = request
  return flag.conditions.every((condition) => {
    switch (condition.type) {
      case 'COOKIES':
        return condition.value === cookies[condition.name]
      case 'HEADERS':
        return condition.value === headers[condition.name]
      case 'DATE':
        return condition.value <= new Date().getTime()
    }
  })
}

const getFeatureFlags = async (request) => {
  const flags = await readFile()
  const activeFlags = flags.reduce((acc, flag) => {
    const isActive = assertConditions(flag, request)
    return isActive ? acc.concat(flag.name) : acc
  }, [])
  return activeFlags
}

export default async function handler(req, res) {
  const flag = await getFeatureFlags(req)
  if (flag.length > 0) {
    return res.status(200).json(flag)
  }
  return res.status(404).json({ message: 'No flags found' })
}
  • readFile reads the JSON file at ./data/flags.json and returns the parsed JSON.
  • assertConditions checks if the conditions of each flag are met.
  • getFeatureFlags method iterates over the list of flags and returns the list of active flags.
  • The handler method is the endpoint. It takes the request and returns the list of active flags. If no active flags are found, it returns a 404 error.

For the sake of simplicity, I'm using a JSON file to store the list of active flags. Feel free to use any type of database or key-value store, as per your preference.

[
  {
    "name": "featureX",
    "desc": "Activate this feature when user is from IN and has BSE1191 cookie",
    "type": "EXPERIMENT",
    "conditions": [
      {
        "type": "COOKIES",
        "name": "featureX",
        "value": "BSE1191"
      },
      {
        "type": "HEADERS",
        "name": "x-geo-location",
        "value": "IN"
      }
    ]
  },
  {
    "name": "featureY",
    "desc": "Activate this feature on Thu Jan 12 2023 00:00:00 GMT+0530 (India Standard Time)",
    "type": "RELEASE",
    "conditions": [
      {
        "type": "DATE",
        "value": "1673461800000"
      }
    ]
  }
]

The above file defines two feature flags. The first is an Experiment Flag, which is activated by a combination of cookie and a location header value. The second is a Release Flag, which is activated when current date is greater than the date value.

Now if we were to CURL the endpoint,

curl --location --request GET 'localhost:3001/api/feature-flags'

{"message":"No flags found"}

Let's try with the first feature flag.

curl --location --request GET 'localhost:3001/api/feature-flags' \
--header 'x-geo-location: IN' \
--header 'Cookie: featureX=BSE1191'

["featureX"]

We see that the feature flag is active for the combination of cookie and location header values. Now let's call this API on the client side.

Client Side

Open the fun-with-flags directory and start by creating a components/feature-control.js file. We'll use the same code for FeatureControl as shown above, with an additional fallback prop.

/**
 * A Feature Flag toggle that renders component based on whether or not flags are active.
 * @param flags a list of flags
 * @param strict true = should activate only when all flags are active. false = should activate if one or more flags are active
 * @fallback a component to render when no flags are active
 * @returns component if flags are active, null otherwise
 */
const FeatureControl = ({ flags = [], strict = false, children, fallback = null }) => {
  const { activeFlags } = useFeatureFlags()
  const activate = strict
    ? flags.every((f) => activeFlags.includes(f))
    : flags.some((f) => activeFlags.includes(f))
  return activate ? children : fallback
}

export default FeatureControl

Next, we'll create a context/feature-flags.context.js file which will be used to provide the list of active flags to the FeatureControl component with the useFeatureFlags hook.

import { createContext, useContext, useEffect, useState } from 'react'

const defaultState = []
const FeatureFlagContext = createContext(defaultState)

const FeatureFlagProvider = ({ children }) => {
  const [featureFlags, setFeatureFlags] = useState(defaultState)

  useEffect((effect) => {
    const fetchFeatureFlags = async () => {
      const response = await fetch('/api/feature-flags')
      const json = await response.json()
      setFeatureFlags(json)
    }
    fetchFeatureFlags()
  }, [])

  const values = {
    activeFlags: featureFlags,
  }
  return <FeatureFlagContext.Provider value={values}>{children}</FeatureFlagContext.Provider>
}

export const useFeatureFlags = () => {
  const context = useContext(FeatureFlagContext)
  if (context === undefined || context === null) {
    throw new Error(`useFeatureFlag must be called within FeatureFlagProvider`)
  }
  return context
}

Let's dive into what's happening in the FeatureFlagProvider component. We have a state variable featureFlags which is used to store the list of active flags. We also have a useEffect hook which will make an API call to the backend to get the list of active flags. The hook useFeatureFlags will be then consumed at the component which needs the list of active flags.

Open _app.js and wrap it with FeatureFlagProvider, so that any child component can access the list of active flags with useFeatureFlags hook.

import '../styles/globals.css'
import { FeatureFlagProvider } from '../context/feature-flag.context'

function MyApp({ Component, pageProps }) {
  return (
    <FeatureFlagProvider>
      <Component {...pageProps} />
    </FeatureFlagProvider>
  )
}

export default MyApp

Next, open pages/index.js and replace the default boilerplate with the following:

import styles from '../styles/Home.module.css'
import FeatureControl from '../components/feature-control'

const Fallback = () => <div>No flags are active</div>

export default function Home() {
  return (
    <div className={styles.container}>
      <FeatureControl flags={['featureX']} fallback={Fallback()}>
        <h1>Feature X</h1>
      </FeatureControl>

      <FeatureControl flags={['featureY', 'featureZ']} strict fallback={Fallback()}>
        <h1>Feature Y</h1>
        <h1>Feature Z</h1>
      </FeatureControl>
    </div>
  )
}

If this looks familiar, that's because we are re-using the code from our Sandbox. We use the FeatureControl component to render the content based on the active flags. We also provide a fallback component to render when no flags are active.

Here is how the end result looks like:

The source is on GitHub.

And that's it! We've built a very basic feature flag engine which can be used to enable features based on various conditions in your application.

When should you consider Feature Flags

  • The decision of when to release new software features is controlled by the business and is independent of the engineering sprints (or KanBan if that's your thing). In such cases, the engineering team has already pushed the feature out to production, but the rollout date is at a later time in the future.
  • To perform a Canary Release, only turning the new feature on for a small percentage of total user-base — an experiment that can be turned off if necessary.
  • You utilize continuous integration/continuous delivery (CI/CD) to ship new features to production very quickly. With feature flags, you can turn off a feature that performs unexpectedly in production without rolling back the code.
  • Controlling entitlements for new features is a key part of your business process. These are long-lived flags, categorized as permission toggle.

Implementing feature flags can seem really simple at first, but it turns out that like with any software, the devil is in the details. It can quickly lead to messy codebase or increased technical debt.

Cleaning up after yourselves

Increased technical debt is a common objection to implementing feature flags. The more feature flags you create, the more code paths, increasing the complexity of your code and making the testing process a lot harder. A higher number of code paths can lead to code-spaghetti and risk the quality of your software. Here are some tips to avoid that.

  • Name your Feature Flags following a standard naming convention across your team. Without it, it is possible to confuse the flags, forget why they were created for, or create flags with the same name — all of which can cause problems and negatively impact your software. Having a good naming convention allows using automated workflows to remove feature flags without needing intervention from a developer, drastically reducing the manual work required in the clean-up process.
  • Clean up as soon as you create a short-lived feature flag. When you create a Pull Request for a short-lived feature, immediately raise another PR, which reverts the original implementation. The reason is that when you create the revert PR, you'd still be having the context of what that short-lived flag is for.
  • Have your CI run two different builds. One with all feature flags active, and another with flags inactive. This drastically reduces the chances of not covering most code paths in automated tests properly. Covering for all combinations of feature flags is a Herculean task, specially on mono-repos and large code-bases. In such cases, it's better to clean-up flags as soon as the feature is deemed complete.

Conclusion

Feature flags allow us to ship code faster with greater confidence. Using Feature Flags, we reduce deployment risk and make code reviews and development on new features simpler, because we can work on small batches efficiently.

Keep in mind though that Feature Flags are not an escape-hatch, allowing your engineering team to ship untested or incomplete code. You should still follow procedures for manual & automated testing and ship features only when it satisfies your team’s and organization’s quality requirements.