Feature flags — best practices
Feature management system requirements, Part 5
This article intends to capture what worked for us after working on a product using a feature management system for over 3 years.
Define a common user context
The issue description
If there are multiple clients or services using feature flags and each client or service creates its user context that is provided for feature flag evaluation, then it causes the existence of multiple variations of user context, which complicates feature enablement.
The following sections will focus on the specific problems that are caused by the issue described above.
Issue: Inconsistent user context key
Some feature management clients might use a different value for the user context key. For example, a mobile client is using the user ID as a key, and backend services are using the user’s email as a key. If that happens, we will see the user twice in the LaunchDarkly. Then we need to add all the user variants to enable or disable a feature flag.
Let’s consider the following user contexts as mentioned in the example above.
{
key: “C56A71D1–5CC5–4607-A515–2352E6BB3C36”,
email: “ondrej.kvasnovsky@example.com”
} vs {
key: “ondrej.kvasnovsky@example.com”,
email: “ondrej.kvasnovsky@example.com”
}
If we allow this situation, we end up with multiple variants for the same user. As a consequence, we will have a hard time selecting a proper user to enable a feature flag, because we will know what user contexts are the clients sending to check the variant of a feature flag.
The only solution to enable a feature flag in this situation is to go to the code, find out what variant of a feature flag that client is creating, and sending to the feature management system. Only then we can enable a feature flag with some level of confidence. This requires a lot of effort and people will likely just add all the user variants to the rule, which is going to cause more chaos.
Adding all the user variants to all the feature flags and segments is making it an operational nightmare. Plus, we will never know for sure if a feature is enabled for specific users because we won’t know if we included all the user variants.
Issue: Variables with similar names
We have shown how an inconsistent user key can lead to frustrating situations. Similar kinds of issues come when different field names are used for the same value.
Imagine you have iOS, Android, Web, and backend services using different naming conventions for user context fields. The iOS is calling a building ID a buildingID
, Android and web are calling it buildingId
, and backend services are calling it BuildingID
.
If there are multiple fields containing the same value, we will end up with redundant rules because, over time, nobody will know what clients are sending what fields and feature enablement becomes a very frustrating experience.
Shared user context definition
The examples mentioned above showed us that we need to define a common use context to avoid user context variations. We cannot let everyone use their user context variation. After all, it makes releasing a feature a nightmare because we never know what variation of user context you need to enable the feature flag for.
The solution is to define a library that specifies and provides the user context. It can also make it easier to create the user context by transforming custom payloads to the user context structure.
let user = User(id: "1", "name": "Ondrej")
let userContext = toUserContext(user: user)
Since the library knows about the user context, it could also help with unifying how we create an instance of the LaunchDarkly client.
Remember, the instance of the LaunchDarkly client needs to be a singleton, there needs to be only one instance per client at the time. We can hide the initialization of a LaunchDarkly client behind a facade like this.
let ffClient = await FeatureClient()
Here is the full code example of showing how the transformation and creation of the feature flag client might look like.
let user = User(id: "1", "name": "Ondrej")
let userContext = toUserContext(user: user)let ffClient = await FeatureClient()let isEnabled = ffClient.isEnabled("my-feature-flag", userContext)
Distribute/share the user context through the system
In case you work in micro-service-oriented architecture, it is useful to share the user context with all the services.
There are a few ways how that could be achieved, this is how we did it:
- we have created a special HTTP header, something like
X-User-Context
that contains the user context payload (e.g. as Base64, wrapped in JWT, or any other way you think is the best for your system) - every service is using a special version of the HTTP client that takes the context and forwards it to another HTTP call the client makes
It is better to propagate the same context to all the services than let each service create its own user context.
Implementation notes
If we are using NodeJS on the backend, we can use AsyncLocalStorage
from async_hook
library to store the user context and get it back so we can forward it once the service calls another service.
If we are using Java and Spring, we can use HandlerInterceptor
to capture the incoming request, extract user context from the HTTP header and store it in ThreadLocal
instance.