How the iOS team at Just Eat built a scalable open-source solution to handle local and remote flags.
Originally published on the Just Eat Engineering Blog.
Overview
At Just Eat we have experimentation at our heart, and it is very much dependent on feature flagging/toggling. If we may be so bold, here’s an analogy: feature flagging is to experimentation as machine learning is to AI, you cannot have the second without the first one.
We’ve developed an in-house component, named JustTweak, to handle feature flags and experiments on iOS without the hassle. We open-sourced JustTweak on github.com in 2017 and we have been evolving it ever since; in particular, with support for major experimentation platforms such as Optimizely and Firebase Remote Config.
JustTweak has been instrumental in evolving the consumer Just Eat app in a fast and controlled manner, as well as to support a large number of integrations and migrations happening under the hood.
In this article, we describe the feature flagging architecture and engine, with code samples and integration suggestions.
What is feature flagging
Feature flagging, in its original form, is a software development technique that provides an alternative to maintaining multiple source-code branches, so that a feature can be tested even before it is completed and ready for release. Feature flags are used in code to show/hide or enable/disable specific features at runtime. The technique also allows developers to release a version of a product that has unfinished features, that can be hidden from the user. Feature toggles also allow shorter software integration cycles and small incremental versions of software to be delivered without the cost of constant branching and merging – needless to say, this is crucial to have on iOS due to the App Store review process not allowing continuous delivery.
A boolean flag in code is used to drive what code branch will run, but the concept can easily be extended to non-boolean flags, making them more of configuration flags that drive behavior. As an example, at Just Eat we have been gradually rewriting the whole application over time, swapping and customizing entire modules via configuration flags, allowing gradual switches from old to new features in a way transparent to the user.
Throughout this article, the term ‘tweaks’ is used to refer to feature/configuration flags. A tweak can have a value of different raw types, namely Bool
, String
, Int
, Float
, and Double
.
Boolean tweaks can be used to drive features, like so:
let isFeatureXEnabled: Bool = ...
if isFeatureXEnabled {
// show feature X
}
else {
// don't show feature X
}
Other types of tweaks are instead useful to customise a given feature. Here is an example of configuring the environment using tweaks:
let publicApiHost: String = ...
let publicApiPort: Int? = ...
let endpoint = Endpoint(scheme: "https",
host: publicApiHost,
port: publicApiPort,
path: "/restaurant/:id/menu")
// perform a request using the above endpoint object
Problem
The crucial part to get right is how and from where the flag values (isFeatureXEnabled
, publicApiHost
, and publicApiPort
in the examples above) are fetched. Every major feature flagging/experimentation platform in the market provides its own way to fetch the values, and sometimes the APIs to do so significantly differ (e.g. Firebase Remote Config Vs Optimizely).
Aware of the fact that itโs increasingly difficult to build any kind of non-trivial app without leveraging external dependencies, it’s important to bear in mind that external dependencies pose a great threat to the long term stability and viability of any application.
Following are some issues related to third-party experimentation solutions:
- third-party SDKs are not under your control
- using third-party SDKs in a modular architected app would easily cause dependency hell
- third-party SDKs are easily abused and various areas of your code will become entangled with them
- your company might decide to move to a different solution in the future and such switch comes with costs
- depending on the adopted solution, you might end up tying your app more and more to the platform-specific features that don’t find correspondence elsewhere
- it is very hard to support multiple feature flag providers
For the above reasons, it is best to hide third-party SDKs behind some sort of a layer and to implement an orchestration mechanism to allow fetching of flag values from different providers. We’ll describe how we’ve achieved this in JustTweak.
A note on the approach
When designing software solutions, a clear trait was identified over time in the iOS team, which boils down to the kind of mindset and principle been used:
Always strive to find solutions to problems that are scalable and hide complexity as much as possible.
One word you would often hear if you were to work in the iOS team is ‘Facade‘, which is a design pattern that serves as a front-facing interface masking more complex underlying or structural code. Facades are all over the place in our code: we try to keep components’ interfaces as simple as possible so that other engineers could utilize them with minimal effort without necessarily knowing the implementation details. Furthermore, the more succinct an interface is, the rarer the possibility of misusages would be.
We have some open source components embracing this approach, such as JustPersist, JustLog, and JustTrack. JustTweak makes no exception and the code to integrate it successfully in a project is minimal.
Sticking to the above principle, the idea behind JustTweak is to have a single entry point to gather flag values, hiding the implementation details regarding which source the flag values are gathered from.
JustTweak to the rescue
JustTweak provides a simple facade interface interacting with multiple configurations that are queried respecting a certain priority. Configurations wrap specific sources of tweaks, that are then used to drive decisions or configurations in the client code.
You can find JustTweak on CocoaPods and it’s on version 5.0.0 at the time of writing. We plan to add support for Carthage and Swift Package Manager in the future. A demo app is also available for you to try it out.
With JustTweak you can achieve the following:
- use a JSON local configuration providing default tweak values
- use a number of remote configuration providers, such as Firebase and Optmizely, to run A/B tests and feature flagging
- enable, disable, and customize features locally at runtime
- provide a dedicated UI for customization (this comes particularly handy for features that are under development to showcase the progress to stakeholders)
Here is a screenshot of the TweakViewController
taken from the demo app. Tweak values changed via this screen are immediately available to your code at runtime.
Stack setup
The facade class previously mentioned is represented by the TweakManager
. There should only be a single instance of the manager, ideally configured at startup, passed around via dependency injection, and kept alive for the whole lifespan of the app. Following is an example of the kind of stack implemented as a static let
.
static let tweakManager: TweakManager = {
// mutable configuration (to override tweaks from other configurations)
let userDefaultsConfiguration = UserDefaultsConfiguration(userDefaults: .standard)
// remote configurations (optional)
let optimizelyConfiguration = OptimizelyConfiguration()
let firebaseConfiguration = FirebaseConfiguration()
// local JSON configuration (default tweaks)
let jsonFileURL = Bundle.main.url(forResource: "Tweaks",
withExtension: "json")!
let localConfiguration = LocalConfiguration(jsonURL: jsonFileURL)
// priority is defined by the order in the configurations array
// (from highest to lowest)
let configurations: [Configuration] = [userDefaultsConfiguration,
optimizelyConfiguration,
firebaseConfiguration,
localConfiguration]
return TweakManager(configurations: configurations)
}()
```
JustTweak comes with three configurations out-of-the-box:
UserDefaultsConfiguration
which is mutable and usesUserDefaults
as a key/value storeLocalConfiguration
which is read-only and uses a JSON configuration file that is meant to be the default configurationEphemeralConfiguration
which is simply an instance ofNSMutableDictionary
Besides, JustTweak defines Configuration
and MutableConfiguration
protocols you can implement to create your own configurations to fit your needs. In the example project, you can find a few example configurations which you can use as a starting point. You can have any source of flags via wrapping it in a concrete implementation of the above protocols. Since the protocol methods are synchronous, you’ll have to make sure that the underlying source has been initialised as soon as possible at startup. All the experimentation platforms provide mechanisms to do so, for example here is how Optimizely does it.
The order of the objects in the configurations
array defines the configurations’ priority. The MutableConfiguration
with the highest priority, such as UserDefaultsConfiguration
in the example above, will be used to reflect the changes made in the UI (TweakViewController
). The LocalConfiguration
should have the lowest priority as it provides the default values from a local JSON file. It’s also the one used by the TweakViewController
to populate the UI.
When fetching a tweak, the engine will inspect the chain of configurations in order and pick the tweak from the first configuration having it. The following diagram outlines a possible setup where values present in Optimizely override others in the subsequent configurations. Eventually, if no override is found, the local configuration would return the default tweak baked in the app.
Structuring the stack this way brings various advantages:
- the same engine is used to customise the app for development, production, and test runs
- consumers only interface with the facade and can ignore the implementation details
- new code put behind flags can be shipped with confidence since we rely on a tested engine
- ability to remotely override tweaks de facto allowing to greatly customise the app without the need for a new release
TweakManager
gets populated with the tweaks listed in the JSON file used as backing store of the LocalConfiguration
instance. It is therefore important to list every supported tweak in there so that development builds of the app can allow tweaking the values. Here is an excerpt from the file used in the TweakViewController
screenshot above.
{
"ui_customization": {
"display_red_view": {
"Title": "Display Red View",
"Description": "shows a red view in the main view controller",
"Group": "UI Customization",
"Value": false
},
...
"red_view_alpha_component": {
"Title": "Red View Alpha Component",
"Description": "defines the alpha level of the red view",
"Group": "UI Customization",
"Value": 1.0
},
"label_text": {
"Title": "Label Text",
"Description": "the title of the main label",
"Group": "UI Customization",
"Value": "Test value"
}
},
"general": {
"greet_on_app_did_become_active": {
"Title": "Greet on app launch",
"Description": "shows an alert on applicationDidBecomeActive",
"Group": "General",
"Value": false
},
...
}
}
Testing considerations
We’ve seen that the described architecture allows customization via configurations. We’ve shown in the above diagram that JustTweak can come handy when used in conjunction with our AutomationTools framework too, which is open-source. An Ephemeral configuration would define the app environment at run-time greatly simplifying the implementation of UI tests, which is well-known to be a tedious activity.
Usage
The two main features of JustTweak can be accessed from the TweakManager
.
- Checking if a feature is enabled
// check for a feature to be enabled
let isFeatureXEnabled = tweakManager.isFeatureEnabled("feature_X")
if isFeatureXEnabled {
// show feature X
} else {
// hide feature X
}
- Getting and setting the value of a flag for a given feature/variable. JustTweak will return the value from the configuration with the highest priority that provides it, or nil if none of the configurations have that feature/variable.
// check for a tweak value
let tweak = tweakManager.tweakWith(feature: <#feature_key#>, variable: <#variable_key#>")
if let tweak = tweak {
// tweak was found in some configuration, use tweak.value
} else {
// tweak was not found in any configuration
}
The Configuration
and MutableConfiguration
protocols define the following methods:
func tweakWith(feature: String, variable: String) -> Tweak?
func set(_ value: TweakValue, feature: String, variable: String)
func deleteValue(feature: String, variable: String)
You might wonder why is there a distinction between feature and variable. The reason is that we want to support the Optimizely lingo for features and related variables and therefore the design of JustTweak has to necessarily reflect that. Other experimentation platforms (such as Firebase) have a single parameter key, but we had to harmonise for the most flexible platform we support.
Property Wrappers
With SE-0258, Swift 5.1 introduces Property Wrappers. If you haven’t read about them, we suggest you watch the WWDC 2019 “Modern Swift API Design talk where Property Wrappers are explained starting at 23:11.
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. Common examples are @AtomicWrite
and @UserDefault
but more creative usages are up for grabs and we couldn’t help but think of how handy it would be to have property wrappers for feature flags, and so we implemented them.
@TweakProperty
and @OptionalTweakProperty
are available to mark properties representing feature flags.
Here are a couple of examples, making the code so much nicer than before.
@TweakProperty(fallbackValue: <#default_value#>,
feature: <#feature_key#>,
variable: <#variable_key#>,
tweakManager: tweakManager)
var isFeatureXEnabled: Bool
@TweakProperty(fallbackValue: <#default_value#>,
feature: <#feature_key#>,
variable: <#variable_key#>,
tweakManager: tweakManager)
var publicApiHost: String
@OptionalTweakProperty(fallbackValue: <#default_value_or_nil#>,
feature: <#feature_key#>,
variable: <#variable_key#>,
tweakManager: tweakManager)
var publicApiPort: Int?
Mind that by using these property wrappers, a static instance of TweakManager
must be available.
Update a configuration at runtime
JustTweak comes with a ViewController that allows the user to edit the tweaks while running the app. That is achieved by using the MutableConfiguration
with the highest priority from the configurations array. This is de facto a debug menu, useful for development and internal builds but not to include in release builds.
#if DEBUG
func presentTweakViewController() {
let tweakViewController = TweakViewController(style: .grouped, tweakManager: tweakManager)
// either present it modally or push it on a UINavigationController
}
#endif
Additionally, when a value is modified in any MutableConfiguration
, a notification is fired to give the clients the opportunity to react and reflect changes in the UI.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.defaultCenter().addObserver(self,
selector: #selector(updateUI),
name: TweakConfigurationDidChangeNotification,
object: nil)
}
@objc func updateUI() {
// update the UI accordingly
}
A note on modular architecture
It’s reasonable to assume that any non-trivial application approaching 2020 is composed of a number of modules and our Just Eat iOS app surely is too. With more than 30 modules developed in-house, it’s crucial to find a way to inject flags into the modules but also to avoid every module to depend on an external library such as JustTweak. One way to achieve this would be:
- define one or more protocols in the module with the set of properties desired
- structure the modules to allow dependency injection of objects conforming to the above protocol
- implement logic in the module to consume the injected objects
For instance, you could have a class wrapping the manager like so:
protocol ModuleASettings {
var isFeatureXEnabled: Bool { get }
}
protocol ModuleBSettings {
var publicApiHost: String { get }
var publicApiPort: Int? { get }
}
import JustTweak
public class AppConfiguration: ModuleASettings, ModuleBSettings {
static let tweakManager: TweakManager = {
...
}
@TweakProperty(...)
var isFeatureXEnabled: Bool
@TweakProperty(...)
var publicApiHost: String
@OptionalTweakProperty(...)
var publicApiPort: Int?
}
Future evolution
With recent versions of Swift and especially with 5.1, developers have a large set of powerful new tools, such as generics, associated types, opaque types, type erasure, etc.
With Combine and SwiftUI entering the scene, developers are also starting adopting new paradigms to write code.
Sensible paths to evolve JustTweak could be to have the Tweak
object be generic on TweakValue
have TweakManager
be an ObservableObject which will enable publishing of events via Combine, and use @EnvironmentObject to ease the dependency injection in the SwiftUI view hierarchy.
While such changes will need time to be introduced since our contribution to JustTweak is in-line with the evolution of the Just Eat app (and therefore a gradual adoption of SwiftUI), we can’t wait to see them implemented. If you desire to contribute, we are more than happy to receive pull requests.
Conclusion
In this article, we illustrated how JustTweak can be of great help in adding flexible support to feature flagging. Integrations with external providers/experimentation platforms such as Optimizely, allow remote override of flags without the need of building a new version of the app, while the UI provided by the framework allows local overrides in development builds. We’ve shown how to integrate JustTweak in a project, how to setup a reasonable stack with a number of configurations and weโve given you some guidance on how to leverage it when writing UI tests.
We believe JustTweak to be a great tool with no similar open source alternatives nor proprietary ones and we hope developers will adopt it more and more.