Andrii is a passionate hands-on engineering leader that finds it equally joyful to help team members grow and find new brilliant ways of solving problems using technology 🙌
In this story I will cover how we automated management of feature flags using one of the feature flag management vendors. And created safe and reliable system to sync feature flags with codebase of the large app. With a goal to reduce risk of misconfiguring or misusing flags and remove manual steps as much as possible.
Feature flags were integrated into Node.js application built with Apollo GraphQL and Typescript. This is important to mention both of these tech details for context of the solution (hint: we used type checks from TS and GraphQL schema to communicate availability of certain flags).
Design and implement an approach that will allow us to separate deployment from release. That means to enable or disable the feature we won’t need a code change and deployment, but a developer, QA, PO, or others can do that directly from the service's control panel.
Solution must be expandable and portable. It must be straightforward and easy to swap a vendor if we want to do so in the future.
Feature flags are a combination of patterns and approaches that allows teams to separate deployment and release by providing an ability to enable or disable a functionality.
The current solution is based on the static files that live with the repo. If we want to enable or disable a functionality we need to update the corresponding feature flag in a codebase and then deploy it. This solution has several downsides.
The decision was made to use the LaunchDarkly service for managing feature flags and to configure roll out of new features. LaunchDarkly was selected because of the experience with this service, however there's plenty more to select from, like ConfigCat, FlagShip and many others.
We implemented the developer-first approach for working with feature flags. It means that feature flags are introduced by developers while coding, created via the LaunchDarkly API during the CI pipeline, and used via the LaunchDarkly control panel by developers, QAs, PMs, Ops, and others.
zeta_graphql
application in the feature-flags.yml
file. Below are an example file, a description of its format, and a JSON schema.yarn generate-flags
.master
branch.A developer needs to define new feature flags in the feature-flags.yml
file in the root of the zeta_graphql
repo. An example of the file’s content:
isNewProductPageEnabled:name: Enable new product page layoutvariations:- value: falsename: Disabled- value: truename: Enableddefault: 0isSingleSignOnEnabled:variations:- value: false- value: truedefault: 0productRecommendationsCount:name: Number of recommended products to show on a pagevariations:- value: 10- value: 20- value: 30default: 10
Feature flags are described as objects in YAML. Where the top-level key is a feature flag key that will be used in the codebase (isNewProductPageEnabled
, isSingleSignOnEnabled
, and productRecommendationsCount
). A feature flag key must match the pattern ^([a-z][a-z0-9])(-.[a-z0-9]+)$.
The variations[].value
and default
properties are required. And the name
and variations[].name
are optional.
The variations[].value
can be one of type boolean number string.
The default
property is of type number and indicates an index of a default variation in the variations array.
Below is a JSON schema to validate feature flags manifest object:
{"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "http://example.com/example.json","type": "object","patternProperties": {"^([a-z][a-z0-9]*)(-[a-z0-9]+)*$": {"type": "object","properties": {"variations": {"type": "array","items": {"type": "object","properties": {"value": {"oneOf": [{ "type": "boolean" },{ "type": "number" },{ "type": "string" }]},"name": { "type": "string" },"description": { "type": "string" }},"required": ["value"]}},"default": {"type": "number"},"name": {"type": "string"}},"required": ["variations", "default"]}}}
There is a single source of truth and the only place where various clients (Web client, iOS and Android Apps) can query feature flags from GraphQL schema of zeta_graphql
app.
GraphQL schema will expose the featureFlags
field that contains all available feature flags with computed values.
All requests that come to zeta_graphql
have the context of the current user using the app. This context is then used to evaluate feature flags via the LaunchDarkly SDK to create personalized experience.
This allows us to segment customers by such info:
There are several GraphQL clients that already use our schema, and there is also a mechanism to distinguish one from another and to segment customers by the application they use.
The GraphQL server expects clients to send the x-zeta-client
header in each incoming HTTP request. And a value of that header is used to differentiate clients.
Consider following GraphQL clients:
The GraphQL server then uses the value of the x-zeta-client
header in addition to other customer info when evaluating feature flag value.
Example of GraphQL results:
{"data": {"featureFlags": {"isNewProductPageEnabled": true,"isSingleSignOnEnabled": false,"productRecommendationsCount": 30}}}
Let’s consider the case of creating, evaluating, receiving, and archiving a feature flag that enables or disables product search via Omega Product Search microservice.
A developer who works on integrating Omega Product Search into item search flow knows that the functionality should be hidden at the beginning and then it will be rolled out to customers progressively. At this point, the developer defines a unique flag key, possible values, and a default value.
The developer describes the flag in the feature-flags.yml
file:
isNewProductPageEnabled:name: Product Search via Omega Product Searchvariations:- value: falsename: Disabled- value: truename: Enableddefault: 0
The listing above tells us that there is the isNewProductPageEnabled
feature flag that can be either true
or false
, and if the flag is not enabled on an environment it’ll be false
(default value).
The zeta_graphql
is written in TypeScript and developers should have a mechanism of type checking for existing feature flags, including flag keys and possible variations to make sure there's no room for human mistake!
We need to provide TypeScript build engine with typings for our feature flag. It can parse the feature-flags.yml
so we need to generate typings ourselves by executing yarn generate-flags
command.
This command parses the feature-flags.yml
file, generates typings, and emits them to the src/feature-flags.d.ts
file which gets picked up by TypeScript automatically.
Here developer tries to pass 'yes' instead of boolean value, and Typescript will reject it at build time.
const isNewProductSearchEnabled = evalFeatureFlag('isNewProductSearchEnabled','yes', // 👈 this is incorrect value//Arguments of type 'string' is not assignable to parameter of type 'boolean'.ts(2345))
In this case developer tries to use a flag that is not present in a Feature Flags manifest file.
const isSsoEnabled = evalFeatureFlag('isSsoEnabled', // 👈 this flag doesn't exist in manifest file// Argument of type '"isSingleSignOnEnabled"' is not assignable to parameter of type '"isNewProductSearchEnabled" | "productResultCount" | "isSingleSignOnEnabled"'.ts(2345)false,)
A developer tried to store evaluation results in a variable of an invalid type.
const productsCount: string = evalFeatureFlag('productResultCount', 10) // 👈 this flag is number and not a string// Type 'number' is not assignable to type 'string'.ts(2322)
Let’s assume that there is already a working item search feature which calls the eCommerce API. The aim of the developer's work is to add functionality to query items from the Omega Product Search.
A developer knows that the functionality of querying products from the Omega Product Search should be hidden from customers at the very beginning, but it shouldn’t block application deployments and releases of other functionalities for teams that adopted Continuous Delivery principles. Moreover, a developer understands that it will be released to customers progressively.
A developer evaluates the feature flag to choose a data source:
products() {const dataSource = evalFeatureFlag( 'isNewProductSearchEnabled', false)? new NewProductSearchService(): new ProductApi()return dataSource.getProducts()}
The code above demonstrates that the feature flag evaluation in the code is as simple as passing a feature flag key and default value.
Once a developer finished his work and it gets merged into the master branch, the CI pipeline starts checking and building the application. The pipeline includes a job for calling the LaunchDarkly API in order to create feature flags introduced during feature implementation.
Back to our example, the CI pipeline job will call the LaunchDarkly API with the request to create a new feature flag with the isNewProductPageEnabled
key, false
and true
variations, and the default value to be false
.
After the successful CI pipeline, feature flags introduced by the developer are ready to be used in the LaunchDarkly control panel.
At this stage, a developer, QA, PO, Ops, and others who have access to the dashboard can configure targeting for the feature in a LaunchDarkly dashboard.
LaunchDarkly provides powerful capabilities for rolling out features by configuring its targeting for individual users or segments of users.
All clients should call the GraphQL server in order to retrieve all available feature flags. The GraphQL server is a single source of truth and it returns already evaluated feature flags for a specific client, partner, site, user, etc.
Let’s consider the following preconditions:
The GraphQL server will expose the new featureFlags field:
type FeatureFlags {isNewProductPageEnabled: Boolean!isSingleSignOnEnabled: Boolean!productRecommendationsCount: Int!}
And clients can query only feature flags they are interested in:
query getFeatureFlags {featureFlags {isNewProductPageEnabledproductRecommendationsCount}}
When functionality is rolled out to 100% of the audience and a feature flag is no longer needed, it can be archived in the LaunchDarkly control panel. Then, when a developer will execute yarn generate-flag
next time, archived feature flags will be removed from the manifest file and TypeScript typings won’t be generated for them.
TypeScript will detect all references of removed feature flags and emit compilation errors about the usage of a non-existing feature flag.
Q: What if the flag is added to feature-flags.yml
but yarn generate-flags
wasn’t executed?
A: The yarn generate-flags
command generates TypeScript typings or feature flags described in the feature-flags.yml
, so developers can safely use them in the codebase by having strict type checking and autocomplete for available variations. In case a new feature flag is added to the feature-flags.yml
but yarn generate-flags
wasn’t executed, a developer won’t have access to the feature flag in the code - TypeScript will emit an error that the feature flag doesn’t exist.
Important to note that in the case above a developer won’t be able to use the feature flags in the code, but that flag will be successfully created in the LaunchDarkly.
Q: What if the flag is removed from feature-flags.yml
but yarn generate-flags
wasn’t executed?
A: In such a case TypeScript will not emit an error that a feature flag doesn’t exist anymore, and a developer will be able to use it in the code. However, the removed feature flag won’t be removed from the LaunchDarkly. This is needed to protect feature flags to be accidentally removed by developers.
Q: What happens if the flag is presented in feature-flags.yml
and yarn generate-flags
was executed, but during CI stage we failed to create a flag in LaunchDarkly via API?
A: Since a feature flag has been introduced in the feature-flags.yml
and the yarn generate-flags
command was executed that means the flag is ready to be used in code. A developer now can evaluate it and fully rely on TypeScript type-checking.
Once the CI stage failed and the flag hasn’t been created in the LaunchDarkly we need to warn developers about the issue, so they will make a decision about how to handle it. There are several possible approaches:
Q: What is the strategy to support multiple clients of GraphQL API?
A: The strategy is described in the Retrieving feature flags section. But long story short, the zeta-graphql
app expects clients to send x-zeta-client
header in each incoming HTTP request. The value of that header should be unique for each type of the client.
The GraphQL server via the LaunchDarkly SDK will use that info when evaluating the feature flags, so customers can be split into segments by a client type.