Feature Flags in Ruby - How we've crafted a gem to rule them all
What are feature flags?
Let's imagine you want to experiment with different application settings and their impact on your customers. The solution you use should support an option to target specific users, and it should be easy to use for a product manager.
Theoretically, developers could deploy new code every time you want to check new settings. Unfortunately, this requires the time and involvement of many people.
You could, alternatively, create a place in the admin panel where specific values could be updated. The problem with that option is that many organizations have multiple applications, and implementing a separate admin panel in each of them isn’t ideal.
What are your other alternatives? Are there any proven solutions in the software development world? Yes—they’re called feature flags.
Feature flags are part of a software development technique that gives you the ability to turn certain functionality on or off, without deploying new code.
Looking from the code perspective, they’re variables used inside conditional statements that allow developers to use different sections of their code.
Implemented in the right way, feature flags are easy to control and can be used by Product Management, Sales, or Marketing teams. You will see an example in one of the following sections.
What are some general feature flags use cases?
Feature flags have been a part of software development for a long time, but only in recent years has their popularity significantly increased. Let’s look at the areas where feature flags could be especially useful:
- Targeting specific users. You can use this option to show the latest feature only to beta testers or get early feedback from chosen customers.
- Gradually revealing new functionality to 100% of users. This technique will help you reduce the risk of releasing a new software version in production.
- A/B testing. You will be able to provide different versions of your application to random users, and use statistical analysis to determine which variation performs better.
- Giving the ability to switch between different providers of a functionality. Sometimes you might want to compare how different providers work for you, or may you have some doubts about the reliability of a service provider. Using a feature flag, you will have an option to make a change in a minute.
- Quicker release cycles. New features can be deployed in parts and hidden under the feature flag. This way, you will avoid the problem of maintaining huge feature branches waiting for weeks to be merged.
How do we use feature flags at Global App Testing?
Currently, we have two separate Ruby on Rails applications, and we wanted to have one solution for both of them.
We decided to create a gem called GatFeatureFlags which gives us a simple interface and can be used across many applications.
GatFeatureFlags.variation(flag_name, user, default_value)
As you can see it’s enough to provide three parameters. The first one is a feature flag name that we’re going to use.
The second one is a user object. Feature flags can be enabled globally for everyone, but sometimes we want to be more precise and enable it for a specific user based on different parameters like an email or a public ID.
The last parameter is a default value. It‘s possible to use an external service that will control the value of a feature flag, and there’s always a chance that an external service will have unexpected downtime. That’s why it’s good to have a safe default value.
Let’s check an example below:
def can_launch_advanced_tests?
GatFeatureFlags.variation("can-launch-advanced-tests", current_organization, false)
end
It’s a simple method where we can give a certain organization an option to execute an advanced type of test. As an example, the feature flag can be controlled by a product manager discussing different solutions with our customers.
Another example shows a case where we can display a new attachment service only to chosen testers to get their feedback:
def gat_attachment_service
GatFeatureFlags.variation("gat-attachment-service", current_tester, false)
end
Let’s have a closer look at our GatFeatureFlags module:
module GatFeatureFlags
class << self
def variation(flag_name, user, default = nil, config = self.config)
config.provider.variation(flag_name, user_details(user, config), default)
end
def configure
self.config = GatFeatureFlags::Config.new
yield config
end
attr_writer :config
def config
@config || raise("GatFeatureFlags is not configured.")
end
private
def user_details(user, config)
config.user_details_factory.call(user)
end
end
end
The module provides our variation method and an additional method responsible for configuration that can be done from any application on which we’ve installed the gem.
The most important parts in the config are the provider and the UserDetailsFactory:
GatFeatureFlags.configure do |config|
config.provider = provider
config.user_details_factory = FeatureFlags::UserDetailsFactory
config.logger = Logger.new(STDOUT, level: Logger::ERROR)
end
GatFeatureFlags.config.provider.connect
We’ve decided to use one of the paid solutions as our main provider (more about it below), but we left the possibility of connecting with any provider we want, as long as there’s a consistent interface implementing the variation method and the connect method that will depend strongly on the provider’s API.
module GatFeatureFlags
module Providers
class YourProvider
def initialize(config_hash)
@config_hash = config_hash
end
def connect
@client = BuildClient.call(@config_hash)
end
def variation(name, user_details, default)
@client.variation name, user_details, default
end
end
end
end
UserDetailsFactory is another interesting class. It gives us the ability to show certain features only to chosen organizations based on their public ID or to target specific testers based on their email address.
It’s also super simple to extend the type of users we’re working with and add classes like OrganizationUser or even a Hash class where params don’t have to be related to the existing model.
module FeatureFlags
class UserDetailsFactory
class << self
def call(user)
send(user_details_sources.fetch(user.class))
end
private
def user_details_sources
{
Tester => :from_tester,
Organization => :from_organization,
}
end
def from_tester(tester)
GatFeatureFlags::UserDetails.new(
key: "tester_#{tester.public_id}",
email: tester.email
)
end
def from_organization(organization)
GatFeatureFlags::UserDetails.new(
key: "organization_#{organization.public_id}",
organization_public_id: organization.public_id
)
end
end
end
end
What are the options to control feature flags?
The code visible above needs to have a provider of a variation value. As is often the case in the software development world, there’s an option to implement something in-house, use an open-source solution, or pay for a product that’s ready to use. We had to make this decision a few years ago, in 2018.
We checked a few paid solutions like Optimizely, LaunchDarkly, Google Optimize, and Roll-out. Besides that, we pondered a lot of questions in the area of build vs buy considerations and we finally chose LaunchDarkly as our feature flags provider. It includes both Ruby and Javascript SDK and it has a lot of well-documented features.
Quite recently, Dynatrace announced that with a consortium of top feature flag management solutions they’ve submitted a new open standard for feature flagging to the Cloud Native Computing Foundation (CNCF).
Dynatrace wants to create a language-agnostic and vendor-neutral standard for feature flagging and they’re planning a release for Fall 2022. The idea is to define a standard API and provide specification-compliant SDKs in various languages, such as Java, Node, .NET, and many others.
It’s a really interesting project that could simplify the way we work with feature flags and it’s worth watching it closely.
What about the interface controlling feature flags?
Below, you can see the interface created by LaunchDarkly. It shows the flag presented in one of our examples. The targeting is on and we’re returning true only for one organization with a specific public ID.
What are the potential problems with feature flags?
We’ve already discussed the positive side of feature flags, but let’s be honest, there must be some downsides, and we can’t simply skip them. Feature flags add more complexity to the software development process. Let’s check where things are affected the most:
- Cluttered code - Many conditional statements in the code can make it hard to read and debug. Overlapping flags could cause unpredictable behavior and making the flags too granular would prolong and complicate the implementation process.
- Technical debt - You need to remember to remove feature flags that are no longer needed. Otherwise, there might be a lot of forgotten code to clean up.
- Different behavior in many environments - It’s technically possible to execute a manual test in a QA environment that uses different feature flag settings than in a production environment.
- Harder testing - Internal tests will have multiple paths to check. End-to-End tests are written for a specific feature flag value, but if anyone can change the flag for any environment, tests might start failing.
- Having more than one source of truth - Checking the code is not enough to know how something works.
How bad could it be if someone left an unused feature flag for a few years in the code?
Knight Capital Group (an American global financial services firm) learned this in the worst possible way. They lost $440 million in just 45 minutes and as a consequence, they were acquired by their rival a few months later.
If you would like to know more about what happened, there is an interesting case study available here.
What are some best practices for using feature flags?
- Make sure to assign a feature flag to the team responsible for its value.
It’s always a team's decision to introduce a new feature flag, and whenever you need to understand how the feature flag works and what the reasons behind creating it were, you’ll be able to easily find a group of people with a broader context. - Save information on who and why changed the feature flag, especially in the production environment.
There’s no room for surprises when it comes to the feature flag value in a production environment. It should be easy to find the person responsible for the last update. - Remember to clean up your flags regularly.
As mentioned above, it’s super important to regularly remove unused feature flags. After a few years, they might be misinterpreted, accidentally changed, or it may simply take a lot more time to analyze and remove them safely. - Use a standardized naming convention and add labels.
It can be useful to include things like a team name, project name, creation date, and an indication of whether the flag is temporary or permanent.
As an example, the name “teamA-new-attachments-service-temp-24-02-2022” gives us a few important pieces of information - who created the flag and when, what the flag is controlling, and an indication that it’s a temporary flag.
It’s also possible to make the name shorter and use labels if your feature flag provider supports them. - Keep the optimal scope of the feature flag.
Each flag should control only one specific functionality. They shouldn’t be overlapping and they shouldn’t leave any doubt about the impact of changes.
Final thoughts
Overall, our experience with feature flags has been really positive. We have been able to develop new features faster. We’ve been experimenting with different application settings and we’ve made decisions based on the real numbers, not just our hunches.
The key is to keep the number of feature flags under control and remove them as soon as they’re no longer needed.