Having separate processes for code deployment and feature releases helps us move faster and with more safety.
Deployment, specially in a distributed, multi-service environment, is a complex operation that has many moving parts, including builds and checks.
Multiple services are usually deployed independently and without a specific order. Deploying services in a set order can be cumbersome and can also add complexity to rollbacks. Deployment is linear by nature: a deployment will take to production all the code changes that were made up until the cut-off commit being deployed.
Features, on the other hand, are created in parallel. Multiple people work on various features that span across services. Ideally features should be available to users only after all the code changes are completed and database migrations and dependencies are deployed. Otherwise you run the risk of broken or half-completed interactions.
We separate these two operations with Feature Flags.
A feature flag controls if a feature is available for a given user.
Feature flags can represent boolean states (”does this user have access to the Broadcast API?”) or enumerable states (”which of these versions of the Broadcast editor this user has access to?”).
Feature flags enable precision rollouts. We can use our feature flag library to check whether the current user should have access to a feature and either run the code (if they have access) or keep the old behavior (if they do not).
While this extra added check makes the code longer, this check is temporary, as we'll discuss shortly.
Feature flags enable release synchronization and help minimize risk. Here are some guidelines of when feature flags should be used:
We use Posthog as our feature flag tool. We give the flag a descriptive name that makes it clear what the feature does and what its value means. For example:
is-broadcast-api-enabled
: evaluates to true
if the user should have access to it, or false
if not.show-broadcast-editor-version
: evaluates to either v1
or v2
.Feature flag inputs can take multiple parameters (e.g., user ID, team ID, etc.). For example, we may want to release features to specific users or all users of specific teams on Resend. We can respectively include the user ID or the team ID as an input when evaluating the feature flag depending on our release intent.
In either frontend applications or backend services, we use helper libraries to evaluate feature flags. These libraries declare a variable that holds the value of the feature flag evaluation (for example, true
or false
).
These feature checks makes our release intent clearer for new behavior while the old version is still in place.
When writing unit tests, we include cases for all possible states of the feature flag. For example, we add test cases for when it evaluates to false
and check that the behavior is unchanged, and test cases for when it evaluates to true
and check that the new changes are in effect. Our testing libraries can be used to force the evaluation of the feature flag to each state.
Using our feature flag management tool, we add rules that change how the feature flag evaluates. While validating our changes, we add our own users and see if new features behave as expected.
If the feature will be tested by beta users, we add them as well and enable beta testing and then collect feedback before moving forward with the release.
Once we're ready to release for all users, we slowly increase the percentage for the feature while monitoring the new feature and any potential issues until we reach 100% of users.
If we detect issues with the feature, we rollback the release as needed.
Our implementation includes an important principle: feature flags are temporary. Flags should not encode business logic that persists indefinitely — they should only control the release of a feature. Lingering feature flag checks makes code more complex and harder to maintain.
Once the feature is released to 100% of users, we may leave it in place temporarily to allow quick rolling back if needed. Depending on the complexity and risk of the release, we may leave it in place a few days or a few weeks.
Ultimately, after a release is complete and stable, we remove all the checks for that feature flag and update test cases accordingly.
Finally, on the feature flag management tool, we delete the flag.