LaunchDarkly - Improving Flag Usage in Code
LaunchDarkly - Improving Flag Usage in Code
Overview
This guide provides best practices and suggestions for improving code that uses
feature flags. These practices can improve both code quality and ease of maintenance.
You can use code in tandem with your feature flags to maintain and improve the
resilience of your process, including improving flag hygiene, giving your team more
flexibility, refactoring flagged code to the degree that you need and no further, and
generally increasing code quality.
Concepts
In order to use this guide effectively, you should understand these concepts:
Teams that use flags need to ensure that their code only references flags which are
currently in use. This practice is known as "flag hygiene." Here is a selection of
techniques that help maintain good flag hygiene.
To learn more about flag hygiene, read Reducing technical debt from feature flags.
You can maintain flag hygiene by ensuring that the temporary flags created for a task
or project are no longer referenced by any code and are archived after they're no
longer in use. Adding this as a requirement to your team's definition of done may help
streamline your codebase, because only the flags you're actively using will be present
in it.
Code standards are easier to maintain with automated checks, such as those
performed by a continuous integration (CI) system. You can even automate some flag
hygiene checks.
Ensure that the code references for your project are up to date by integrating the ld-
find-code-refs tool with your code pipeline.
Merge conflicts are a common source of friction when you make code changes.
Adding flag logic in the middle of a function increases the chances that both that
addition and its removal later will conflict with other changes.
To reduce the chance of conflicts, refactor both the flag logic and the code for each
variation into their own functions. Keep the refactoring simple so that the overall time
spent implementing the flag is short.
Here's an example to demonstrate this factoring tactic. The example code is taken
from a fictional app which has a search feature which queries a back-end search
engine. In the existing code, the function getSearchResults does the work of
sending the search query to the backend, checking for errors and fetching the results.
Here is an example:
pseudocode
The app's engineering team is preparing to migrate to a new, improved search engine.
To prepare for the migration, the team has created the flag use-new-search-
engine to tell the app which search back-end to use. The code needs to include code
for querying both the old and the new search engines.
Inserting the flag logic in the middle of getSearchResults would make it even more
complicated than it is already. Instead, they extract the relevant code and move it to
new functions. They take the code which sends the query to the search engine (section
2 in the code above) and also code which processes errors and results (sections 3 and
4), because the new engine returns errors and results in a different format.
The team moves these lines of code, along with code for the new search engine and
code to check the flag, into their own functions:
pseudocode
25
26 // This function queries the OLD search engine and processes the respon
27 function queryOldEngine(query, context) {
28 engineURL = SEARCH_ENGINE_URL + '/search';
29 response = sendRequest(engineURL, query);
30 if ([Link].starts_with("SEARCH ERROR")) {
31 [Link]("Search failure on old engine", response, context);
32 return new Error("There was an internal search error.");
33 } else {
34 // turn response into result list, clean up the results, etc.
35
36 // ... results logic goes here...
37
38 return results;
39 }
40 }
41
42 // This function queries the NEW search engine and processes the respon
43 function queryNewEngine(query, context) {
44 engineURL = NEW_SEARCH_ENGINE_URL + '/query';
45 response = sendRequest(engineURL, query);
46 if ([Link] == "error") {
47 [Link]("Search failure on new engine", response, context);
48 return new Error("There was an internal search error.");
49 } else {
50
51 // turn new-engine response into a result list
52
53 // ... results logic goes here...
54
55 return results;
56 }
57 }
COPY
The migration is expected to last a few weeks. Because the new feature is wrapped in a
feature flag, the team can integrate the new search engine code before it's been fully
tested and optimized. Now, they can spend time improving the code with real
production data. Because the new and old search engine code is separated into
different functions, they can make those improvements without changing shared code.
In addition, it's easier to add unit tests for the new code because they can call it
directly without needing to create mock flag states.
y g g
When it's time to retire the old search engine and archive the feature flag, the cleanup
is minimal and less likely to cause merge conflicts.
The code is not intended for long-term maintenance. After the migration is
finished, the duplicates are deleted.
We recommend the common refactoring adage known as the rule of three: don't
refactor for deduplication until code is duplicated three or more times.
There are various techniques you can use to make sure that your feature flags help
accomplish what you want to do with code.
Here are some guidelines you can follow to make sure you write high-quality code:
Avoid passing flag state in a call interface
When you use a feature flag to change the behavior of a component, perform the flag
evaluation inside the component.
It can be tempting to add a dedicated parameter to the call interface and send the flag
state from the outside. However, changing the component's interface means that all
the component's callers have to be changed too, and then changed again once the
flag is removed.
If we moved that call out to getSearchResults and sent the flag value to
executeSearch as an argument, the abbreviated code would look like this:
pseudocode
COPY
getSearchResults has given it more responsibility that it didn't need, and removed
the benefits of the refactoring.
In this example, the changed interface only had one caller. When changing functions
and modules with many callers, there's even greater benefit in keeping the interface
the same. This is especially important given the other risks inherent in changing
heavily-used code.
One way to avoid this problem is to use LaunchDarkly's custom roles feature to restrict
who can access the flag.
This can make it harder to change a flag, which can be good in some cases, but it also
increases the friction in your development process. Instead of making change more
difficult, it's better to improve the robustness of your code.
The following example uses a service that sends batches of data through a processing
pipeline. Each batch must go through three processing steps: normalization,
relabeling, and storage.
A dedicated service performs each of these steps. The storage step is always the last,
but the normalization and relabeling can happen in either order. Each service is
responsible for sending the data batch to the next service.
There are two possible orders in which the processing steps can be performed. The
order is chosen using a feature flag called process-order which has variations
normalize-first and relabel-first.
1 Normalization
1. Normalization
2. Relabeling
3. Storage
1. Relabeling
2. Normalization
3. Storage
Each service is responsible for sending its output to the next step.
It's tempting to have each service evaluate the process-order flag after it's
processed and use this to decide the next step. However, if the flag is flipped in the
middle of processing, this could cause problems.
If the flag is set to normalize-first, the batch starts at the normalization service.
Before that service finishes processing, someone flips the flag to enable the relabel-
first process. When the normalization service finishes processing, it'll evaluate the
process-order flag and use the relabel-first ordering, in which the
normalization step is followed by storage. This data batch should be sent to the
relabeling service because it hasn't been relabeled, but it will be sent to storage
instead.
Conclusion
In this guide, we covered: