A better way to add feature flags
See original GitHub issueThe Problem
Currently, we declare and inject a configuration class via the dependency injection (DI) layer whenever we need to define a feature flag.
data class PhoneNumberMaskerConfig(
val proxyPhoneNumber: String,
val phoneMaskingFeatureEnabled: Boolean
) {
companion object {
fun read(reader: ConfigReader): PhoneNumberMaskerConfig {
return PhoneNumberMaskerConfig(
proxyPhoneNumber = reader.string("phonenumbermasker_proxy_phone_number", default = ""),
phoneMaskingFeatureEnabled = reader.boolean("phonenumbermasker_masking_enabled", default = false)
)
}
}
}
This is problematic for a few reasons:
- It is a lot of overhead to create a new config class if one does not already exist for a feature.
- The configuration objects are scoped to a particular package, but sometimes a feature flag cuts across multiple packages. This leads to leakage of information across boundaries.
- It is very cumbersome to inject an entire class just to get access to a feature flag.
- There is no way for us to find all the feature flags being used across the codebase easily.
We need a better solution, one that can solve these issues while retaining the benefits of the existing solution:
- Be able to configure these flags in tests
- Be able to switch between local and remote flags
- Override these flags for local development
Proposal
We define features using enums, and provide a default value.
enum class Feature(
val enabled: Boolean,
val remoteConfigKey: String = ""
) {
SecureCall(false),
Telemedicine(true, "telemedicine_enabled")
}
Once this is done, we can define a singleton which can be used throughout the app to get access to the feature flag values:
object Flags {
// Set this during app/test initialization
lateinit var remoteConfig: ConfigReader
fun isEnabled(feature: Feature): Boolean {
return when {
feature.remoteConfigKey.isBlank() -> feature.enabled
else -> remoteConfig.boolean(feature.remoteConfigKey, feature.enabled)
}
}
}
With this, we can avoid all the boilerplate around creating a class and injecting it and simply call Flags.isEnabled(SecureCallEnabled)
to check whether a feature is enabled or not. All our feature flags will also be defined in a single place.
The only thing missing from the list of desired benefits is a way to override the values in tests, which can be solved by maintaining a hash map inside the Flags
object.
var overrides = mutableMapOf<Feature, Boolean>()
fun isEnabled(feature: Feature): Boolean {
return when {
feature in overrides -> overrides.getValue(feature)
feature.remoteConfigKey.isBlank() -> feature.enabled
else -> remoteConfig.boolean(feature.remoteConfigKey, feature.enabled)
}
}
Overriding this value in tests is very simple.
@Test
fun `the feature is overriden in this test`() {
Flags.overrides[Feature.SecureCall] = true
}
What does everyone else think? Is this something that appeals to us as a team? Any other solutions people can think of?
Issue Analytics
- State:
- Created 3 years ago
- Reactions:3
- Comments:6 (6 by maintainers)
Top GitHub Comments
Yes, we might still use the same remote configuration service to manage the flags, but the way we access feature flags versus remote config values will be different. The way to access remote config values will remain unchanged for now.
Could you also please think of possible problems we might have with this approach?