Abstract factory pattern for feature flags
Recently, I was working on a feature which needed to be controlled by a feature flag. The feature had several classes which each needed to integrate with different parts of the app. Each class should do some work after various events happen in the app when the feature is enabled. Nothing should happen when the feature is disabled.
When I started writing the first class I put the feature flag check right alongside the code that implements the logic. Something like this:
class MyNewFeatureSignupWorker(flags: FeatureFlags) {
fun onSignup() {
if (flags.isEnabled(MY_NEW_FEATURE)) {
// Do stuff
}
}
}
This comes with a few drawbacks. Firstly, it isn’t following the single responsibility principle. The class is responsible for checking the feature flag AND for the logic. This is increasing the complexity of the class. If we already have some complex logic, adding another if statement is only making it more complex. I also now need to test that the logic happens when the flag is enabled and that it doesn’t when it’s disabled, therefore increasing the complexity of the unit tests. Finally, I was going to create more classes to integrate with other parts of the app. Continuing this pattern would mean duplicating the feature flag check across multiple classes and creating more points of failure.
I decided it would be better to keep the logic separate from the feature flag. To do this I extracted an interface for my class and created two implementations - the real implementation for when the flag is enabled and a no-op implementation for when the flag is disabled. This allows me to move the feature flag check out of the class doing the logic and into a class only responsible for creating the classes. Let’s call this a factory.
interface MyNewFeatureSignupWorker {
fun onSignup()
}
class MyNewFeatureFactory(flags: FeatureFlags) {
fun createMyNewFeatureSignupWorker(): MyNewFeatureSignupWorker {
return if (flags.isEnabled(MY_NEW_FEATURE)) {
MyNewFeatureEnabledSignupWorker()
} else {
MyNewFeatureDisabledSignupWorker()
}
}
}
Remember, I said I would be creating more classes to integrate with more parts of the app. I’m still going to be duplicating the feature flag check for each class that I need to create.
class MyNewFeatureFactory(flags: FeatureFlags) {
fun createMyNewFeatureSignupWorker(): MyNewFeatureSignupWorker {
return if (flags.isEnabled(MY_NEW_FEATURE)) {
MyNewFeatureEnabledSignupWorker()
} else {
MyNewFeatureDisabledSignupWorker()
}
}
fun createMyNewFeaturePurchaseWorker(): MyNewFeaturePurchaseWorker {
return if (flags.isEnabled(MY_NEW_FEATURE)) {
MyNewFeatureEnabledPurchaseWorker()
} else {
MyNewFeatureDisabledPurchaseWorker()
}
}
etc...
}
This duplication is tedious and requires that we write two unit tests for each class we need to create. My ideal goal when working with any feature flag is to only call it in one place.
I could avoid this duplication by extracting an interface for the factory with two implementations - one which only creates the real classes and one which only creates the no-op implementations. This means that I only need to do the feature flag check once where I’m creating the factory. Let’s call this an abstract factory.
interfact MyNewFeatureFactory {
fun createMyNewFeatureSignupWorker(): MyNewFeatureSignupWorker
fun createMyNewFeaturePurchaseWorker(): MyNewFeaturePurchaseWorker
etc...
}
class MyNewFeatureFactoryProvider(flags: FeatureFlags) {
fun getFactory(): MyNewFeatureFactory {
return if (flags.isEnabled(MY_NEW_FEATURE)) {
MyNewFeatureEnabledFactory()
} else {
MyNewFeatureDisabledFactory()
}
}
}
Now, I only check the feature flag once and only need to write two unit tests.
So is the code better than when I started? I’ve introduced more classes, but now each one has a single responsibility and is therefore less complex. Each class can focus on the logic it needs and doesn’t need to worry about feature flags. We’re only checking the feature flag in one place, which is less error prone than sprinkling it throughout each class. This also makes my life easier when it comes to cleaning up the feature flag. Once the feature has rolled out, I only need to clean up code in the factories. The classes implementing the actual logic remain unchanged, which is much safer.
The keen observer will notice that I’ve implemented the abstract factory pattern.