stefanjaindl

Checkout blog posts about various topics helpful to software developers everyday life

blog

I have once implemented a complete feature flag architecture for an iOS app. Such an architecture is important to switch features on or off, even remotely so that no new build is required. This blog post describes how the architecture was implemented.

First a general Feature protocol and an implementation of it is provided:

protocol Feature {
    var key: String { get }
}

struct FeatureFlag: Feature {
    let key: String

    init(key: FeatureKey) {
        self.key = key.rawValue
    }
}

Then, we define an enum FeatureKey, which represent the unique features within the app:

enum FeatureKey: String, CaseIterable {
    case firstFeature
    case superFeature
    case uniqueFeature
...
}

The FeatureFlagProviderType enum represents the different sources where feature flags come from. In this case it is debug, release, remote (for switching features on/off remotely) and test.

enum FeatureFlagProviderType {
    case debug, release, remote, test
}

Next, the FeaturePriority struct defines feature orders, when more than one FeatureFlagProviderType is applicable to a feature:

struct FeaturePriority {
    static let minPriority = 1
    static let mediumPriority = 5
    static let maxPriority = 10
    static let testPriority = 15
}

Then the protocol FeatureFlagProvider requires that priority as well as the type. Implementers must provide methods that determine whether it defines that feature, and if so, whether it is enabled:

protocol FeatureFlagProvider: class {
    var priority: Int { get }
    var type: FeatureFlagProviderType { get }

    func isFeatureEnabled(_ featureKey: FeatureKey) -> Bool
    func hasFeature(_ featureKey: FeatureKey) -> Bool
}

The feature flag providers should be instantiated via a factory:

final class FeatureFlagProviderFactory {
    static func createProvider(type: FeatureFlagProviderType, with featurePairs: [FeaturePair] = [], userDefaults: UserDefaults? = nil, remoteSync: Remote? = nil) -> FeatureFlagProvider {

        switch type {
            case .debug: return DebugFeatureFlagProvider(userDefaults: userDefaults ?? .standard)
            case .release: return ReleaseFeatureFlagProvider()
            case .remote: return ContentfulFeatureFlagProvider(featurePairs: featurePairs, remoteSync: remoteSync)
            case .test: return TestFeatureFlagProvider()
        }
    }
}

We will look in detail at the debug and release providers. The ReleaseFeatureFlagProvider has min priority, so that it can be overruled by remote flags. In this implementation it provides valus for all features, whether they are generally enabled or not:

//Used in release version
final class ReleaseFeatureFlagProvider: FeatureFlagProvider {

    internal let priority = FeaturePriority.minPriority
    internal let type = FeatureFlagProviderType.release

    func isFeatureEnabled(_ featureKey: FeatureKey) -> Bool {
        switch featureKey {
         case firstFeature:
            return true
         case superFeature:
            return false
         case uniqueFeature
            return true
        }
    }

    func hasFeature(_ featureKey: FeatureKey) -> Bool {
        switch featureKey {
        case firstFeature:
            return true
         case superFeature:
            return true
         case uniqueFeature
            return true
        }
    }
}

The DebugFeatureFlagProvider should only be added for debug/dev builds. Features can be switched on or off by UserDefaults settings. It may be handy to provide a UI for doing so:

final class DebugFeatureFlagProvider: MutatingFeatureFlagProvider {

    private let newFeaturesInitiallyEnabled = true
    private let userDefaults: UserDefaults
    internal let priority = FeaturePriority.mediumPriority
    internal let type = FeatureFlagProviderType.debug

    init(userDefaults: UserDefaults) {
        self.userDefaults = userDefaults
    }

    func isFeatureEnabled(_ featureKey: FeatureKey) -> Bool {
        switch featureKey {
        case .routing: return userDefaults.bool(forKey: featureKey.rawValue, defaultValue: false)
        default: return userDefaults.bool(forKey: featureKey.rawValue, defaultValue: newFeaturesInitiallyEnabled)
        }
    }

    func hasFeature(_ featureKey: FeatureKey) -> Bool {
        return true
    }

    func setFeatureEnabled(feature: Feature, enabled: Bool) {
        userDefaults.set(enabled, forKey: feature.key)
    }
}

Next Post Previous Post