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.
Bigger picture
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.
Why did we decide to try feature flags?
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.
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.
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.
Requirements
The implementation of feature flags introduced essential requirements to ensure smooth integration. These included interaction between frontend and backend, efficient management and adaptability to future projects.
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.
Why did we choose GrowthBook?
User and permission management:
GrowthBook allows us to effectively manage users and their permissions, ensuring appropriate access levels for different team members.
Feature flag creation and modification:
With GrowthBook, we can easily create and modify feature flags, enabling controlled releases and targeted customization of features.
Environment management:
GrowthBook supports the creation and management of different environments, facilitating seamless testing and deployment processes.
Well-documented documentation:
GrowthBook provides comprehensive and well-documented resources, making it easier for our team to understand and utilize the tool effectively.
Easy integration of SDKs:
GrowthBook offers straightforward integration with software development kits (SDKs), simplifying the process of incorporating the tool into our existing infrastructure.
Self-hosted capability:
GrowthBook allows us to host and manage the tool internally, giving us more control and ensuring data privacy and security.
Docker setup
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
Administration panel
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.
React implementation
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/>
};
NestJS implementation
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
}
}
Summary
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.
Top comments (0)