We use feature flags to be able to quickly and safely develop features without disrupting other teams at Freetrade.
While making changes on the Android and iOS apps we will enable features only for staff and our beta testers so we can get feedback in our live environment as we develop them.
We work in three verticals, which means we need to work on the same parts of the client apps and server at the same time, and we require extensive testing to ensure our new features are working as expected. Each team can be developing more than one feature in parallel so we require feature flags to split out different journeys through the app.
An example of usage was implementing the Open Banking flow into the client apps and testing the integration in production for over a month before releasing the feature to end users by enabling the feature flag for everyone.
We needed to test different parts of the user experience through the client app which meant restricting the open banking flow to only test users and not breaking the existing top-up method for users in production. Being able to test the Open Banking flow in production was invaluable to solving all issues up front before releasing it to all users.
Put simply, a feature flag is a gate which checks a true or false value and controls whether to allow a user through a particular journey through the app. Let’s look at what a feature flag is and an example of how it can be used:
Let’s look at when a user logs in and they usually see an Account Overview screen. Our team is adding a new Account Homepage with even more information on it. We will add a new Feature Flag which, when enabled, will direct users to the new Account Homepage, rather than the current screen:
```kotlin
loginButtonClick
.withLatestFrom(flagService.isFeatureEnabled(“RedirectToHomepageOnLogin”))
.subscribe { (click, redirectToHomepageOnLogin) ->
when (redirectToHomepageOnLogin) {
true -> router.goTo(AccountHomepageScreen())
else -> router.goTo(AccountOverviewScreen())
}
}
.addTo(disposable)
```
We covered how we use reactive programming at Freetrade in another blog post, but essentially what is going on here is when the user clicks the Login button - we get the latest value of the feature flag for the user, and if it’s enabled they are directed to the Account Homepage screen otherwise they are directed to the old Account Overview screen. Remote configuration setup only allows feature flag changes infrequently, so we use Firestore to make flag changes instant, so changing the flag in the database sees the immediate effect in the client app.
We default all feature flags to `false` in production at the global-level, so when we are developing the feature we can push this code into production and it won’t have any impact on the users until the feature flag is enabled.
This is a really powerful way of being able to test our new screen as it lets a select few members of staff try out the feature against the real stock market before the feature is released to everyone.
There is a design choice and a compromise to make between blocking off whole or partial features from every user and allowing some users through.
What was interesting for me when I joined Freetrade is that the app contains a whole lot more than meets the eye.
All features that are in development are in the app that everyone gets from the App Store, but they just can’t see everything until the feature flags are enabled. Our feature flag approach follows the Release Toggles from Martin Fowler’s Feature Toggle blog post which is how we separate our feature releases from our code deployment by enabling features after they have been deployed into production.
We made a choice to try and use the feature flag as an entry point into the feature we are adding. This means that we don’t have a lot of complicated conditional logic in our codebase and each individual user flow is deterministic in it’s behaviour. We can see this choice in our previous example that we put our feature flag in the entry point to the two screens, rather than adding all the information into one screen and deciding what to show and hide within the screen.
We structure our feature flags on multiple levels: we have global, user and override flags. Global-level flags reveal the feature for all of our users, user-level flags reveal the feature for individual users, and override flags reveal the feature to a percentage of our total users. As we’ve grown, we’ve started rolling almost all features over the course of a few days using override flags which helps us to catch any last minute errors before it becomes unmanageable for us.
We can build a feature and test it in production without exposing it to all of our user base, this is very powerful in that we can be certain in the feature before rolling it out and we know that we can avoid having to merge code with other teams when working on a shared screen as our feature flag separates the new logic to the existing code. We also have the added advantage that given the review time lag on the app stores, this gives us more flexibility to avoid hotfixes.
Our build process rolls out the feature flag to an increasing percentage of users, which enables us to identify issues sooner and then stop rolling out a feature if we need to or we can quickly apply a hotfix without our entire user base having access to a broken feature.
We can also utilise A/B testing, an approach for sending users through two different journeys in the app and comparing the outcome through analytics to determine the more successful journey. We combine the analytics in each screen that users see to make informed product decisions. Because we split out screens and user journeys through different feature flags, we can enable the feature flag for a select percentage of users and compare the success rates of each journey.
Once you’ve set up your first feature flag, it becomes really easy to add more and it can spiral into creating more than necessary. Having a lot of feature flags comes with a cost to maintain them within the codebase and having to add extra tests to cover all possible paths can be cumbersome.
Feature flags should introduce a temporary behaviour and be replaced completely when necessary, there is certainly an overhead for having to remove the code which is no longer required once the feature is enabled for all users.
These can be mitigated by making feature flag management part of the release process, when a feature flag is enabled globally then the code that checked for that flag should be removed. You can add an expiry date on the flag to stop the feature being used after a certain time frame, this gives time to develop the feature while also making sure that the roll out does not get so delayed that the flag loses its intended purpose.
You can see how powerful of a concept having feature flags in our code is - it allows us to develop faster, roll out features with fewer crashes and enables us to safely test in our production environment.
We see having feature flags as an inventory to our codebase, we utilise them in every new feature we add as part of our development cycle and we make them part of our rollout process so we do not get overloaded with legacy code.
We're always on the lookout for engineers to help build our mobile apps. If the above sounds fun, check out our careers page to find out more.