Building a lightweight feature flagging system

Ship your code to production with confidence with this enum based feature flagging system

Why do we even need feature flags?

this article was originally posted in 2018 on my medium blog here

While we build our apps, it’s often necessary to ship code to the App Store, but you don’t want your users to use it yet because it’s not production ready or its meant to go live at a specific date. Why ship to the App Store when the code isn’t ready yet, you ask? This is often the case if your team is following a continuous delivery pipeline. Instead of having a feature branch, all new code is merged into the develop branch and kicks off a build. With multiple members on a team, you need to be able to release features only when they are complete, fully QA’d and are ready to go into the wild 🐯

Lets talk code 👨🏼‍🚀

enum FeatureFlag: String, CaseIterable {
    case featureOne
}

extension FeatureFlag {

    var isEnabled: Bool {
        switch self {
        case .featureOne:
            if isRunningInAppStore() {
                return false
            }
            return featureFlagValue()
        }
    }
    private var key: String {
        return "com.companyname.featureflag." + rawValue
    }
    private func featureFlagValue() -> Bool {
        return UserDefaults.standard.bool(forKey: key)
    }
    func enable() {
        UserDefaults.standard.set(true, forKey: key)
    }
    func disable() {
        UserDefaults.standard.set(false, forKey: key)
    }
}

See the full gist here:

By using swift enums, we have a very nice API. To use this anywhere in our codebase is easy.

FeatureFlag.featureOne.isEnabled  //to check if a feature is enabled
FeatureFlag.featureOne.enable() //to enable a feature
FeatureFlag.featureOne.disable() //to disable a feature

For this specific example, we are using UserDefaults to persist the feature flag. This doesn’t have to be the case at all. Some of the flags can easily check against your bundled plist, a third party service or your own remote server. This gives us a ton of flexibility. We don’t have to couple ourselves to a specific persistence strategy 🤓

As a bonus, we can always add extra properties to our enum. It’s often the case that you want a debug menu somewhere in your app where you can easily toggle each feature flag from a menu. Our FeatureFlag enum can then be extended with a few properties

var showInSettingsMenu: Bool {
    switch self {
        case .featureOne: return true
        case .featureTwo: return true
        case .featureThree: return true
    }
}

var settingsMenuCellTitle: String {
    switch self {
        case .featureOne: return "👨‍👩‍👧 Enable feature one"
        case .featureTwo: return "📁 Enable feature two"
        case .featureThree: return "Enable feature three"
    }
}

Simply add the properties you need to your enum. Easy

Lets Ship it

Well, not yet. We need to be 100% sure that our feature flags won’t accidentally be set to true in production when we actually aren’t ready to ship them. So let’s do the responsible thing and write some tests first.

import XCTest

class FeatureFlagTests: XCTestCase {
    
    func setup() {
        super.setUp()
        AppConfiguration.🔥alwaysReturnAppStoreEnvironment🔥 = true
    }
    
    override func tearDown() {
       super.tearDown()
       AppConfiguration.🔥alwaysReturnAppStoreEnvironment🔥 = false
        for flag in FeatureFlag.allCases {
            flag.disable()
        }
    }
    
    func testThatFeatureOneFlagIsOff() {
        XCTAssertFalse(FeatureFlag.featureOne.isEnabled)
    }
}

Notice the AppConfiguration.🔥alwaysReturnAppStoreEnvironment🔥 which isn’t covered in this blog post. It allows us to mimic an AppStore environment for our tests. Depending on your particular feature flag you will most likely need to write some extra tests. For example, checking if the debug settings menu is hidden in production and other UI related checks. So write enough tests here to give you enough confidence to ship.