When building modern applications, things tend to move quite quickly. When you add multiple developers to your team, things really start to get going. With enough developers, you’ll find you have multiple in-progress things, that aren’t quite ready for production. How do you let the developers keep working at their most efficient speed, whilst keeping your app in a constantly releaseable state?
Enter feature flags.
You’ll have heard the term feature flags before.
Here, I’m referring to the process of having a feature ( and all of its code ) hidden behind a flag that can be enabled on an app build. You may choose to allow developers or testers to adjust these at run-time, but the important part is that you can have a hard off for features that aren’t quite ready for production.
When you do this, you can let developers merge in their code without working on long-lasting feature branches, without causing any changes to the actual product. This allows you to let your main branch serve as a constantly evolving, releasable snapshot of your code.
Let’s look at an example of this.
Here we have an app called Cortado. We’ve got a basic menu and a my orders screen, and our product owners have asked us to add a loyalty screen with a virtual reward card. This will require a whole new tab being added, and some small additions elsewhere such as the my orders screen linking to the reward card.
This feature will take a little while to build, and it will be chunked up in the following steps ( that may be chunked up smaller again ) to make sure that everyone has something to do, and the work can keep progressing through a kanban board.
- Add the new tab
- Add the reward card UI to the new page
- Add a list of recent orders to the new page
- Add a mini reward card to the top of the my orders page
This work is going to be worked on whilst another team is building out a new payments feature that will make changes to the payment and order screens. They’re going to start whilst we work, and finish ahead of us.
There’s a few ways we could go about managing the branches here, and they all have a few downsides. By the end of this post, we’ll have a nice middle ground that should keep product & tech happy.
Option 1: Feature branches
The first solution, is to have two long lasting feature branches that the teams work from. This means that the work is kept entirely seperate, and the first time both sets of code see eachother will be the second merge.
When we do this, main stays always ready to release, and hotfixes can easily be applied. Each rocket repesents a meaningful release that could be done if all goes to plan.
There’s some problems here that we need to consider:
- Integration testing can only be done when the loyalty branch merge is complete, and any issues found will delay the launch of that feature.
- The Payments branch will be ahead of Loyalty in terms of the main branch, so it could have various features in it that the loyalty one isn’t aware of.
- Any bug fixes applied to main are potential sources of conflict with both branches, and conflict resolution can take a substantial amount of time — espeically with an iOS xcodeproject.
- If bugs are found with the payments feature, they will delay the ability to create a build with loyalty in, another possible way to delay the launch.
Option 2: Short lived branches
An alternate solution, is to have small, single task branches off the main branch that don’t stick around for long. This helps keep pace strong, but kills the ability to create a release whenever desired.
Here, you can see that we have a lot of different branches, merging together more often. This opens the door for more frequent integration testing, makes it easier to spot bugs, and stops huge conflicts — although doesnt quite get rid of them entirely.
This time, there’s less problems to consider, but they’re still there:
- You can’t release main until the features are finished. If you have to do a bug fix, you’d have to branch from main before this work started, and then merge that back in later.
- The code thats in main is in a potentially un-stable state, and it’ll certainly be unfinished. Our example first task would create an empty loyalty tab on the first merge.
- Many little branches could mean many little conflicts.
- Following a refined process, each individual merge would require individual code reviews, which would potentially slow down developer velocity.
Option 3: Short lived branches with feature flags
Using the exact same approach before, here we’re going to make one little change. The first task for each feature, is to add a feature flag that can be turned on and off when we run builds. This means that from that point onwards, the main branch will behave as if these features aren’t being worked on, despite their code lying their dormant.
You also gain a little bonus ability — if you release a feature and you don’t like it, you can just turn it off.
There’s still some risks here — but very few:
- Whilst this code is dormant, its still there, and can cause problems. This requires a good eye in code reviews to avoid releasing part finished features.
- You have to remember to tidy up these flags, or you’ll get into situations where you’re turning on muliple to just see one feature.
- Sometimes a feature flag is impractical, especially if you’re doing something in a storyboard , meaning even when this featrue is turned off you might end up seeing little bits of new stuff.
If we chose option 3, we can get working with both teams, as soon as we add those feature flags. Both teams set off, build their features, and when they send a build to a tester they simply turn on their feature. When the work starts on some of our secondary features such as the mini loyalty UI on the my orders screen, that can be in its own feature flag.
If any fixes are needed, or emergency releases, they can be done confidently no matter what state the two features are in.
Feature flags can and should be built simply to begin with. You can extend these later with things like the previoulsy mentioned run-time customisation, but its not needed to get started.
We simply have a list of features and a basic switch that enables or disables them. Defaulting to false creates a safety first situation that should help prevent features that aren’t ready from sneaking into your next release. A great way to extend this would be to store values in user defaults, core data, or even make them configurable in an .xcconfig file so you can have targets for each feature.
Using a feature flag
Having the feature flag is only the beginning. Now, we actually have to use it.
Feature flags tend to expose spaghetti code very quickly, as you’ll notice loads of these
if Feature.loyalty.isEnabled's scattered throughout your codebase. If this starts to happen, consider a refactor to de-couple your code before you start to add these.
We’ll look at three different examples here, a View in SwiftUI, a ViewModel, and a UIKit view.
SwiftUI’s view builders seem like they were built for these. You can now use control flow in your view code, and add views based on the flag being enabled.
Here’s our view before we have a feature flag and the new code.
Now, lets add our feature flag. All we have to do is make sure to add the @ViewBuilder attribute, and a quick if statement for our loyalty feature.
This adds our new feature and shows the new view for us, in one if statement.
If we go into our feature file, and make the following change:
We’ll see the following when we run our app.
There’s absolutely no trace of our new feature, and we could release our app with that tab being invisible — great!
Something a little more complicated, is the changes you might have to make to your viewmodels. If you don’t use MVVM, feel free to skip this section.
There’s a couple steps we need to take here to make sure that we can properly use feature flags with view models. The first, is to extract a protocol for our View model. The example one we’re going to use fetches a list of orders, and will be extended after this to fetch the users stamps.
Extracting a protocol, is easy. By doing this, we can change the type our view references, and inject in a different view model depending on what state we’re in. A good approach if you’re building apps where you know you’ll be using feature flags, is to build this protocol driven way from the start.
Now we’ve done that little refactor, we can build our new view model, that has some extra little properties on it.
Here, we’ve been able to hide away the extra features behind both a feature flag and a type check. This means that in a higher level view we can inject the correct view model and also ensure the feature flag is set correctly. When this feature is eventually released, we can add the properties to the protocol, use our new view model only, and remove both the checks in our if statement — neat!
Something i’ve come across a few times with feature flags, is that they tend to fall apart when you have a storyboard. You can’t change the storyboard and hide that behind a feature flag, so you have to make a compromise:
- Add the UI and make sure it gets turned off in code — which is easy to forget
- Change the UI by adding it in code — creating a mix of programatic and interface builder designs
- Accept that some of the UI will be different even with the feature turned off — this will happen in a feature where you’re not just adding something, you’re also rebuilding existing UI
My solution, is to use nibs for my view controllers. There’s many advantages of these ( which we won’t get into here ) but a great one — is being able to progranatucally pick which nib you want for a given view controller. This allows you to have two nibs for the same view controller, or simply present a whole different one instead. Lets look at those options in code.
Here is a typical way of presenting a nib-backed view controller in code.
Now, lets see the two different ways we can use our feature flag to change this code. The first option is to simply change the nib in our initialiser to use different UI.
The second way, is to give this responsibility to our co-ordinator or presenting view controller, this is arguably the correct way to do it, as a view shouldn’t have to decide if its showing a new or old version.
In both examples, we’ve managed to remove the need to create a whole second view controller, by just having a seperate UI file that backs the new UI. This allows for the old UI to remain completley intact.
Feature flags are a powerful tool that can help drive confidence in your product. Whatever technology you use to build your app, there’s a way to get feature flags in. We’ve talked about some advanced usage, like allowing users to change them at run time, or using xcconfigs, and these are all great additions you can make once you have these integrated into your development process.
Once you’re comfortable, you can even look at extending these. What if the answer to is this enabled came from a web service, or you addded A/B testing functionality to them?
There’s a lot of opportunity — give it a try.