Feature flags enhance the responsiveness of development teams to market trends and user feedback, making it easier and safer to deliver new features. Discover how to implement feature flags step by step and create a more modular and maintainable codebase.
A QUICK SUMMARY – FOR THE BUSY ONES
Scroll down to the whole article to get deeper into each step of feature flags implementation.
TABLE OF CONTENTS
In the realm of contemporary software development, the ability to promptly respond to user feedback, stay in line with market trends, and adapt to evolving requirements holds immense significance.
This is precisely where feature flags come into the picture.
Feature flags, also commonly referred to as feature toggles, are a programming technique that empowers developers to activate or deactivate specific features without the need to deploy new code. This approach provides developers with flexibility, reduces potential risks, and facilitates the process of experimenting with new functionalities.
Before we delve into the implementation details, let's take a step back and grasp the larger context about feature flags.
In a rapidly evolving technological landscape, user preferences can change unexpectedly, and market demands may take sudden turns. Feature flags emerge as a strategic tool that empowers development teams to quickly adapt to these changes.
By decoupling the release of features from code deployments, you achieve the capability to make real-time decisions regarding which users have access to specific features. This provides a smooth user experience and facilitates an iterative approach to development.
Imagine being able to test new features on a subset of users before a full-scale launch, while collecting invaluable feedback and metrics along the way. This controlled strategy allows you to make data-driven decisions, improve features and avoid potential pitfalls. What's more, feature flags make A/B testing easier, where different versions of a feature are compared to identify the one that resonates most effectively with users.
From a technical point of view, function flags encourage a more modular and maintainable code base. By encapsulating new features behind flags, you can reduce the complexity of code merging, isolate possible problems and ensure that unfinished or unstable features do not disrupt the user experience.
We wanted to try out and play with feature flags within an internal project, without customer involvement.
This controlled environment provided a safe space to experiment, learn, and improve our skills, all without pressure from external stakeholders. It acted as an invaluable testing arena where we could thoroughly understand the concepts and mechanics of feature flags, preparing the ground for their eventual integration into our customer-facing projects.
Our decision to try out feature flags was driven by several factors.
In particular, we observed how prominent industry leaders and corporations were skillfully using feature flags to optimize their development processes, which sparked our curiosity about the potential benefits they could offer our own projects.
Additionally, our goal to follow trunk-based development principles heavily influenced our choice.
Using feature flags aligned perfectly with our aim of continuously integrating new code into the main branch. By hiding unfinished features from users, we could smoothly add code to the main branch without causing major disruptions and without compromising the user experience.
The implementation of feature flags introduced essential requirements to ensure smooth integration.
These included:
Our approach prioritized the retrieval of flags from both the frontend and backend. This dynamic control mechanism made it easy to simply activate or deactivate features.
The administration panel played a key role in enabling the management of flags.
For implementation, we decided to host the solution independently in a Docker container. This decision gave us more control and increased security during the deployment process
Considering cost-effectiveness was key, which led us to evaluate different tools and technologies to provide the best-fit solution.
Our choses solution for feature flag management was GrowthBook.
It’s a feature flagging and experimentation platform that allows for managing feature rollouts and conducting experiments.
GrowthBook allows us to effectively manage users and their permissions, ensuring appropriate access levels for different team members.
With GrowthBook, we can easily create and modify feature flags, enabling controlled releases and targeted customization of features.
GrowthBook supports the creation and management of different environments, facilitating seamless testing and deployment processes.
GrowthBook provides comprehensive and well-documented resources, making it easier for our team to understand and utilize the tool effectively.
GrowthBook offers straightforward integration with software development kits (SDKs), simplifying the process of incorporating the tool into our existing infrastructure.
GrowthBook allows us to host and manage the tool internally, giving us more control and ensuring data privacy and security.
By default, Growthbook stores its data in a mongo database. We use the bitnami image because it comes with a basic configuration out of the box.
The admin panel and all core functions come directly from the Growthbook image.
We also use proxy image. This speeds up synchronization when flags are added or switched, e.g. flag switching is immediately reflected in our application.
mongo:
image: docker.io/bitnami/mongodb:6.0
restart: always
env_file:
- ./.env
volumes:
- 'mongodb_master_data:/bitnami/mongodb'
ports:
- 27017:27017
growthbook:
container_name: "growthbook"
image: "growthbook/growthbook:latest"
ports:
- "4000:3000"
- "4100:3100"
depends_on:
- mongo
env_file:
- ./.env
volumes:
- uploads:/usr/local/src/app/packages/back-end/uploads
proxy:
container_name: "growthbook_proxy"
image: "growthbook/proxy:latest"
ports:
- "4300:3300"
depends_on:
- growthbook
env_file:
- ./.env
volumes:
uploads:
mongodb_master_data:
driver: local
mongo:
image: docker.io/bitnami/mongodb:6.0
restart: always
env_file:
- ./.env
volumes:
- 'mongodb_master_data:/bitnami/mongodb'
ports:
- 27017:27017
growthbook:
container_name: "growthbook"
image: "growthbook/growthbook:latest"
ports:
- "4000:3000"
- "4100:3100"
depends_on:
- mongo
env_file:
- ./.env
volumes:
- uploads:/usr/local/src/app/packages/back-end/uploads
proxy:
container_name: "growthbook_proxy"
image: "growthbook/proxy:latest"
ports:
- "4300:3300"
depends_on:
- growthbook
env_file:
- ./.env
volumes:
uploads:
mongodb_master_data:
driver: local
Let's take a closer look at the administration panel and go through the functionalities we use.
To start using the feature flags, the first step is to properly configure the environments.
In our case, we decided on three environments: a production environment, a dedicated environment for testing purposes and all activities performed during the continuous integration process, and a staging environment.
After configuring the environments, the next task is to configure the SDKs. Each SDK corresponds to a specific environment.
And finally - flags. For our test case, let's create a boolean flag to test a new method for calculating the lead time distribution metric. Each flag must be assigned to at least one environment where toggling will be possible.
First, for better DX, let's create an enum with the feature flags that are available in our applications and packages:
export enum FEATURE_FLAG {
FEATURE_FLAG = 'new-ltd-mertric-calculation',
}
To use a Growthbook in a React application, we need to create an instance of the Growthbook and pass it to the context provider that wraps our application. In addition, our application needs to know what flags are available at any given time and if their state changes, this should be reflected in the application. This is why loadFeatures is used in the useEffect hook with the autoRefresh option.
import { GrowthBook, GrowthBookProvider } from "@growthbook/growthbook-react";
const App = () => {
const gb = new GrowthBook({
apiHost: import.meta.env.VITE_FEATURE_FLAGS_API_HOST,
clientKey: import.meta.env.VITE_FEATURE_FLAGS_CLIENT_KEY,
enableDevMode: import.meta.env.DEV,
});
useEffect(() => {
gb.loadFeatures({ autoRefresh: true });
}, []);
return (
<GrowthBookProvider growthbook={gb}>
// rest of stuff
</GrowthBookProvider>
);
};
To check if the flag is toggled, use useFeatureIsOn hook. This hook will return a Boolean value based on the state of the flag. Then we can render a new version of the metric or do whatever is required.
import { useFeatureIsOn } from "@growthbook/growthbook-react";
const FriendlyComponent = () => {
const isNewLtdMetric = useFeatureIsOn(FEATURE_FLAG.NEW_LTD);
return isNewLtdMetric ? <New/> : <Old/>
};
On the backend in the metrics tool, we use NestJS. Let's dive into the feature flag module.
First of all, create a token to manage dependency injection.
export const FEATURE_FLAG_TOKEN = Symbol('FEATURE_FLAG_TOKEN');
This token is used to create FeatureFlagService , which returns a configured instance of Growthbook.
Note that this service can be request scoped. It is not, because we have modules that are used outside the request scope. Using the request scope module in other modules with different scopes will break them.
export type FeatureFlagService = GrowthBook;
export const featureFlagProvider = {
provide: FEATURE_FLAG_TOKEN,
useFactory: async (
configService: ConfigService,
): Promise<FeatureFlagService> => {
const gb = new GrowthBook({
apiHost: configService.get<string>('FEATURE_FLAGS_API_HOST')!,
clientKey: configService.get<string>('FEATURE_FLAGS_CLIENT_KEY')!,
enableDevMode: true,
});
await gb.loadFeatures();
return gb;
},
inject: [ConfigService],
};
Finally, let's add this provider to the module as follows.
@Module({
providers: [featureFlagProvider],
exports: [featureFlagProvider],
})
To use such a service, we simply need to inject a token into another provider.
export class LeadTimeMetric {
constructor(
@Inject(FEATURE_FLAG_TOKEN)
private readonly featureFlagService: FeatureFlagService,
){}
calculate(...) {
if (this.featureFlagService.isOn(FEATURE_FLAG.NEW_LTD)) {
return; // new version of metric to be calculated
}
return; // old version of metric to be calculated
}
}
Feature flags are definitely a powerful tool that makes it easier and safer to deliver new features. It takes time to get used to this new way of implementing new functionality. It is important to note that we have only scratched the surface of this tool's potential. I encourage you to consider using feature flags. In a landscape where adaptability and responsiveness are most important, feature flags offer a path that is worth exploring.
Our promise
Every year, Brainhub helps 750,000+ founders, leaders and software engineers make smart tech decisions. We earn that trust by openly sharing our insights based on practical software engineering experience.
Authors
Read next
Popular this month