How Feature Flags can help your mobile app deployment

Feature flags have revolutionized mobile development by allowing features to be individually enabled/disabled at any point, even after they’ve already been rolled out to users.

Modern mobile development moves quickly; here at ImmobiliareLabs we release a new version of our iOS app every 2 weeks.

If you are lucky enough to live the moment of a team growth, you will also see the a special moment where everything starts slowing down because you have multiple in-progress things that aren’t quite ready for production.

It’s a typical scaling problem where you need to rethink how you and your team perform everyday jobs (if you are experiencing this transition you should really take a look at  “Mobile Apps at Scale” by Gergely Orosz).

Great features takes time to build.
How do you let the team working efficiently – free to experiment – whilst keeping the app in a releasable state?
Feature Flags is one of the strategies you can use to better handle the increase of traffic in your team repositories.

What’s a Feature Flag?

Some people call them “Feature Toggles”, some other “Feature Switch” or “Feature Controls”. All of them are names for a feature management solution that enables developers or product manager to change product’s functionality without deploying new code.

You can consider a feature flag as a decision stop-point in your code that can change the behavior of your product. You may choose to allow developers/tester/product team to adjust these values at runtime.

“(…) feature flagging is to experimentation as machine learning is to AI, you cannot have the second without the first one”
JustEat Tech Blog

Common Scenarios

Feature flags are helpful for numerous scenarios typical with mobile development teams.

Avoid Long-Lasting Branches

In this environment you can let developers merge in their code without working on long-lasting branches (every dev hates resolving boring merge conflicts every time for days or, worse, entire weeks).

Feature flags allows to keep your MRs small & focused, the main branch can be continuously updated and you can release nightly builds with no worries (*)

Reduce the risk

Bugs happen — even with the best developers and rigorous testing techniques.

Imagine this: after months of work-in-progress your team have just release a new amazing feature and are you’re exicited for your users to try it out. Unfortunately things don’t go the way you thought they would and you found an unexpected bug.
Now you can either deploy an hot fix or revert the feature.

However deploying mobile apps is not easy as for web apps or sites and with the app review you may wait 1-2 days. That’s terrible as you risk losing users.
You have to do something immediately!
With remote controlled flags you can temporary disable the feature while taking your time to release the hot fix.

A/B Testing

Feature flags can be used to activate different experiences for sets of users. You can, for example, divide your user base into several sections and measure the conversion performance in order to pick the higher product conversions, engagement, growth, subscription rates, or revenue.

Product Campaigns/Early Access

Product and marketing teams can safely roll out features when they are ready, from remote. They can grant access to a feature to a group of elite users or new users to drive engagement.

Our Development Scenario

At ImmobiliareLabs we use feature flagging to gate in-progress features so we can continue to build & test them without exposing them to end users before they’re ready.

Merge Requests are short and focused and enter into the main branch even if they are not complete yet. Obviously they are disabled by default until everything is ready, but team can continue the standard release cycle.

To make it effortless, at iOS team we’ve developed an in-house opensource library named RealFlags to handle feature flags without the hassle. The idea behind the library is to use the best of new Swift language features to create a powerful abstraction layer over multiple providers you can use to setup your experiment lab.
RealFlags born with Swift in mind and the vast majority of the core concepts we used to make it are introduced in the current major 5.x.

In the post below  I’ll describe the overall architecture and how it works with code samples and suggestions based on our experience.
On behalf of my group, I should like to thank  several open source libraries who inspired the work below: JustTweak by JustEat, Override by Yahoo and FeatureFlags by Ross Butler.

What you want from Feature Flags

As I said a the beginning of the post, mobile development moves quickly; the product still evolving, constraints change, your code easily to become a legacy code you need to refactor.
Our new Swift app born less than 4 years ago but during this period we rewrote several parts: feature flags helped us to make this process gradual. In fact we moved from old to new implementations in a way transparent to the end user.

But a product is not only a tech affair (some devs tend for forget it).
As a continuous experiement your product team would to introduce new features, enable/disable existing ones via A/B testing and so on.

So the feature flag engine must fulfill these requirements:

  • It’s simple
    The goal is to hide the complexity as much as possible. Your team must be able to define and manage feature flags easily.
    In fact you should indirect encourage devs to use them.
  • It’s a gateway
    It must act as a gateway for unfinished features. Devs/qa/product must be able to test them development process is going while the release cycle can continue with no stops.
  • It encourages good habits
    Flags must be self-describing and easy to be organized: you may end with many different feature flags in place. The likelihood of getting lost in confusion is high and you don’t certainly want to waste your time searching where the flag is and what it means. The library itself should call on good habits.
  • It’s abstract
    You don’t want to rely on a specific service only to waste time and resources rewriting all from the scratch when your product team will decide to move all to something else.
    Third-party SDKs are not under your team control and it’s different to support multiple feature flags providers.
  • It support different datatypes
    Typically a feature flag is just a boolean value but you need to use Int, Float, String or your own structs, enum or classes! You should be able to save and set them easily.
  • Feature flags are discoverable
    In order to simplify your dev job and allows qa/product to play with flags you should prepare a great user interface to browse and alter values.

RealFlags acts as orchestration library both when you need to choose as organize your flags and how to query for values.

GitHub – immobiliare/RealFlags: ❇️ Feature Flagging framework in Swift sauce (support dynamic data providers, like FirebaseRemoteConfig)
❇️ Feature Flagging framework in Swift sauce (support dynamic data providers, like FirebaseRemoteConfig) – GitHub – immobiliare/RealFlags: ❇️ Feature Flagging framework in Swift sauce (support dyna…

Introduction

RealFlags provides a simple UI to interact and modify your flags.
Feature flags are organized in collections; you can define collections by using your own criteria.
Once done you can add some sort of hidden menu section of your app and tell the browser to show them.

Below you can see the RealFlags menu which perfectly fits in our Developer Tools menu. This section still visible only when app is running in debug or from a TestFlight installation.

private lazy var shouldShowDevMenu: Bool = {
  #if DEBUG
  return true // Xcode
  #else
  guard let path = Bundle.main.appStoreReceiptURL?.path else {
    return false // AppStore
  }
  return path.contains("sandboxReceipt") // TestFlight
  #endif
}()
You can use the following code to show your Debug Menu to present apps’ feature flagsg

We have different flags and we have choosen to organize them by conpetence area with a final collection dedicated to remote controlled flags.

0:00

/

This is our development menu (visible only when app is running via Xcode or installed via TestFlight. The Feature Flags browser allows anyone involved in development phases to alter values.

At the time of this writing RealFlags last version is 1.2.0; you can use it via CocoaPods or SPM (more infos).

Setup

It’s time to steup your stack; now you’re ready to create your first collection. Suppose you wanna make a section dedicated to the user’s experimental features.
We’ll call it UserExperiments: in this section product team can enable or disable some amazing features. Some flags may also be controlled from remote using Firebase Remote Config.

As good habit RealFlags allows you to declare the blueprint of the collection (a collection is a struct conforms to FlagCollectionProtocol) including the inner flags. While you can create collections of collections I strongly suggest to avoid it for the sake of clarity.

public struct UserExperiments: FlagsCollectionProtocol {
   // ....
   public init() {}
}

A feature flag is just a variable wrapped by @Flag decorator. Technically spearking it’s a Property Wrapper which defines all the attributes and metadata of the flag. In In short, a property wrapper is a generic data structure that encapsulates read/write access to a property while adding some extra behavior to augment its semantics while avoid code duplication.

Suppose you want to create a enableSSON flag which allows the user to use single-sign-on to login inside the app.
By default this flag is turned off:

public struct UserExperiments: FlagsCollectionProtocol {
   
   @Flag(key: "sson", default: false, description: "Enable Single-Sign-On")
   var allowsSSON: Bool
   
   public init() {}
}

allowsSSOn is a Bool flag which by default is false (RealFlags supports Int, String, Data, Date, URL, Dictionary, JSON and all the data types conforms to Codable) which .
The description field is used to describe the flag inside the browser so be careful when you fill it.
We have also specified key: it represent the string RealFlags will use to query every Data Provider (you can configure it as you wish).
You can also set more configuration parameters.

A Data Provider is just a source of data; when you query for a value of a flags, RealFlags query – in order – every provider getting the value for that key. The first one who respond with a non nil value stops the chain with that value.
If no data provider is able to respond (or you have not defined any provider) the default value is therefore returned.

Load a Collection

Since the code above it’s only a blueprint of our collection, now we need to load it. FlagsLoader is a Generic-aware class responsible to read (via mirroring) the structure of a collection.
Typically you would use a single loader to handle a single collection.
When you instantiate a loader you will also specify – in order – the Data Providers you wanna use to query each flag’s value (note: you can still exclude some data providers per single flag or query a specific data provider type when you want to get the value of a flag).

Actually you can choose two different Data Providers (but you can easily make your own as you need):

  • LocalProvider: it’s a writable provider. It allows you to load and set values locally inside the sandbox by just provide the URL to a file destination.
  • FirebaseRemoteProvider: it’s read-only provider; allow you to get values directly from a pre-configured session of Firebase (keep in mind: your Firebase must configured via FirebaseApp.configure() before calling this provider).

Our FFService is a simple helper class inside our app which holds the value of any loader:

public class FFService {

  public let main = FFService()
  
  public let user: FlagLoader<UserExperiments>

  private init() {
     let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]..appendingPathComponent("user_flags.xml"))
     user = FlagsLoader(UserExperiments.self,
                                  description: .init(name: "User Features", description: "Experimental lab"),
                                  providers: [
                                     FirebaseRemoteProvider(),
                                     LocalProvider(localURL: fileURL)
                                  ])
  }

}
The FlagLoader is used to load the blueprint of a collection. It’s the entry point to query each flag’s value by using defined data provider.

The order of the data provider in the init defines the query priority. When fetching a flag, the engine will inspect the chain of providers in order and pick the flag from the first configuration having it.

Query for values

Now you are able to query for User Experimental features by calling userExpsLoader: thanks to DynamicMemberLookup Xcode will autocomplete for you each flag defined by the collection. Just magic!

if FFService.main.user.allowsSSON {
   // do something
} else {
   // do something else
}
Once called RealFlags will check for a valid value of allowsSSON (“sson”) inside each data provider starting by looking in RemoteConfig, then locally inside the app.

Sometimes you may want to get the value of the flag inside a specific provider. In this case you may refer to the projected value.
In this case we will get the value of LocalProvider by ignoring the Firebase which has an higher priority in our loader:

if FFService.user.$allowsSSON.flagValue(from: LocalProvider.self) {
   // this is the value of local provider for this flag
}
Using projected value you can query your flag more granularly.

As we said we may have nested collections:

struct UserExperiments: FlagCollectionProtocol {
    @FlagCollection(description: "A nested collection")
    var secrets: SecretsExperiments

    @Flag(key: "sson", default: false, description: "Enable Single-Sign-On")
    var allowsSSON: Bool
}

struct SecretsExperiments: FlagCollectionProtocol {
    @Flag(default: false, description: "Risky feature!")
    var boomFeature: Bool
}

In this case you can query for boomFeature using:

if FFService.main.user.secrets.boomFeature { ... }

But, what’s the key queried for boomFeature? If you don’t specify the key for a flag the snake_case version of the variable name combined with the path down to the variable itself. In this case the key queried is secrets/boom_feature.

Set Values

LocalProvider is an example of writable Data Provider: this mean you can alter a flag value and it persist between app launches.

FFService.main.$allowsSSON.setValue(true, providers: [LocalProvider.self])
You can change value of a flag at runtime by using setValue() and specify the target provider

Since we maintain several target applications for different countries we have used setValue() to keep a single blueprint file of definitions while altering default values inside a conditional init:

public class FFService {

  init() {
     // ....
     switch target {
     case .es:
        overwriteESFlags()
     case .it:
        overwriteITFlags()
     ...
  }
  
  private func overwriteESFlags() {
     user.allowsSSON.setValue(true, provider: [LocalProvider.self])
     // ...
  }

  private func overwriteITFlags() {
     user.allowsSSON.setValue(false, provider: [LocalProvider.self])
     // ...
  }
}
You can use setValue() function to setup default values for feature flags without duplicating blueprint files (and change the default property).

Computed flag

Sometimes you may need to create a feature flag where the value is a combination of other flags or runtime values you need to evaluate dynamically.
This is the perfect example to use the computedValue which is just a callback function called before query any data provider.
Returning a non nil value here stops the chain.

Our suggestion is to limit the use of this pattern in order to kept the code readable. We have introduced it to handle several edge cases we have in our app.

struct UserExperiments: FlagCollectionProtocol {

  @Flag(default: false, computedValue: MiscFlags.computedRememberLogin)
  var rememberLogin: Bool
    
  private static func computedRememberLogin() -> Bool? {
      Language.main.code == "it" && allowsSSON == true
  }
  
  ...
}
rememberLogin is active only for IT locale and when SSON is also active

Show the browser

We started looking at the feature flag browser. But we haven’t said how to show it. That’s simple:

public class FFService {
   // ...
 
   private var flagsController: FlagsBrowserController

   func showFlagsBrowser() {
      flagsController = .create(loaders: [user, ...])
      mainController.present(flagsController, animated: true, completion: nil)
   }
}
Presenting FlagsController is simple; you need to pass each FlagLoader you have used and you want to get listed in the UI.

Conclusion

In this article I illustrated how RealFlags can be a great help in adding an abstract yet flexible approach to feature flagging. Currenty we’re using it in our flagship product; if you want to contribute, provide additional features or provider check out the Github project page!

The same article is also available on my Medium account and it was published by Better Programming.

(*) that’s not a promise 😉

Leave a Reply

Made with love in Rome, Italy