Feature flags service
Let's talk about feature flags. Feature flags are a way to enable or disable a feature in your application. They are used to testing new features, to roll out new features to a subset of users, or to disable a feature in case of an emergency.
Case study
A case study is a detailed study of a specific subject, such a service or feature. It is a way to show how Farfetched can be used to solve a real-world problem.
The code in it is not supposed to be ready to use "as is", it is just an example of how Effector and Farfetched can be used to deal with a specific problem.
Kick-off
Let's say you have a new feature that you want to test — dynamic favicon that changes after some activity in the application. It's a cool feature, but you don't want to release it to all users at once. You want to test it first, do some math calculating profit increase, and then release it to a subset of users.
So, you need to create a feature flag for this feature:
import { createStore } from 'effector';
const $dynamicFaviconEnabled = createStore(false);
The default value is false
, so the feature is disabled by default. However, our manager wants to have the ability to enable the feature for a subset of users. So, we need to fetch the value from the server that will be responsible for feature flags management. We will do it later.
Now you can use this flag to enable feature in the application:
import { createEvent, createEffect, sample } from 'effector';
const somethingHappened = createEvent();
const changeFaviconFx = createEffect(() => {
const fav = document.querySelector('[rel="icon"][type="image/svg+xml"]');
fav.href = 'other-favicon.svg';
});
sample({
clock: somethingHappened,
filter: $dynamicFaviconEnabled,
target: changeFaviconFx,
});
I'm not familiar with Effector, could you explain a bit?
Sure!
createStore
creates Store. It's a place where you can store a value and subscribe to changes.
const $dynamicFaviconEnabled = createStore(false);
createEvent
creates Event. It's a way to send a signal and notify subscribers.
const somethingHappened = createEvent();
createEffect
creates Effect. It's a way to perform a side effect, like sending a request to the server or changing favicon.
const changeFaviconFx = createEffect(() => {
const fav = document.querySelector('[rel="icon"][type="image/svg+xml"]');
fav.href = 'other-favicon.svg';
});
sample
creates connection between somethingHappened
and changeFaviconFx
. It means that changeFaviconFx
will be called when somethingHappened
happened only if $dynamicFaviconEnabled
contains true
.
sample({
clock: somethingHappened,
filter: $dynamicFaviconEnabled,
target: changeFaviconFx,
});
So, it's time to designs portable and reusable feature flags service for the whole application that will be used by all developers in our frontend team!
Design
Let's list all the requirements:
- Fetch the value of the feature flag from the server after the feature module is initialized and batch requests to the feature flags server to make it more efficient.
- Pass some application context to the feature flags server, so it can decide which value to return.
- Allow using default value for the feature flag before the real one is loaded or in case of an error. Also, we need a way to distinguish between the default value and the real one in the application.
- Validate the value of the feature flag on the client side to prevent unexpected structure that can break the application.
Bonus requirement: let's make it friendly for application developers. They should not care about the implementation details of the feature flags service and be able to use it independently in different products inside the application.
So, to meet these requirements, we need to create a function that will accept configuration of the particular feature flag (when we have to fetch it, what is the default value, etc.) and return something that can be used in the application.
const { $value: $dynamicFaviconEnabled } = createFlag({
/* ... */
});
Why does a function return object with $value
field instead of single $value
?
For now, we use only $value
in the application, but we can add more fields in the future. For example, we can add $loading
field that will be true
while the value is loading and false
otherwise. So, in general, it's a good practice to return an object from a factory instead of a single value. It allows us to add more fields in the future without breaking the API.
We do the same for createFlag
arguments by passing an object instead of a list of arguments. It allows us to add more fields in the future without breaking the API.
It's time to find out arguments of the createFlag
function.
key
— a unique key of the feature flag. It's used to identify the feature flag in the feature flags server.fetchOn
— we need to know when to fetch the feature flag. It can be an event or list of events. For example, we can fetch the value of the feature flag when the application is initialized.defaultValue
— we need to know what is the default value of the feature flag. It will be used before the real value is loaded or in case of an error.contract
— we need to know how to check the value of the feature flag. It will be used to prevent unexpected structure that can break the application. Since Farfetched has a built-in structure to do it, we can use it here.
import { runtypeContract } from '@farftehced/runtypes';
import { Boolean } from 'runtypes';
const { $value: $dynamicFaviconEnabled } = createFlag({
key: 'exp-dynamic-favicon',
defaultValue: false,
contract: runtypeContract(Boolean),
fetchOn: applicationInitialized,
});
TIP
We use runtypes
as a library for creating Contracts there. However, you can use any library you want. Read more in the tutorial.
Implementation
Let's split our implementation into two parts:
- internal implementation that will handle fetching, context passing, etc.
- public API that will be available in user-land
Fetching
First we have to create Query to receive information about feature flags from remote source.
import { createJsonQuery, declareParams } from '@farfetched/core';
const featureFlagsQuery = createJsonQuery({
params: declareParams<{ flagKeys: string[] }>(),
request: {
method: 'POST',
url: 'https://flagr.salo.com/',
body: /* TODO: formulate request's body */,
},
response: {
contract: flagrResponseContract,
},
});
INFO
In this receipt Flagr is used as a feature flags service, but it affects only fetching section, so you can you whatever you want.
We use createJsonQuery
to create a query that will send a request to the feature flags server and receive a response in JSON format. We use declareParams
to declare that the query accepts an object with flagKeys
field. It's a list of feature flags keys that we want to receive from the server.
Let's add a rule to start the Query:
import { createEvent, createStore } from 'effector';
// We will use this event in `createFlag` function to register new keys
const registerNewKey = createEvent<string>();
// Let's store all registered keys for the application
const $requiredKeys = createStore<string[]>([]).on(registerNewKey, (keys, key) => [...keys, key]);
// We will trigger it in `createFlag` function to start fetching of the feature flag
const performRequest = createEvent();
// Connect all together
sample({
// every time when performRequest is triggered
clock: performRequest,
// take all $requiredKeys
source: $requiredKeys,
// transform them into an object with a single `flagKeys` field
fn: (flagKeys) => ({ flagKeys }),
// and start featureFlagsQuery with it
target: featureFlagsQuery.start,
});
That's it! Now we can start the query when we need to fetch the value of the feature flag.
Context passing
For sure, we need to pass some application context to the feature flags server, so it can decide which value to return. For example, we can pass the user ID or preferred language to the server to decide whether to enable the feature flag for the user or not.
import { combine } from 'effector';
// External stores that we want to pass to the feature flags server
// it have to be filled outside of the feature flags service
const $userId = createStore<string | null>(null);
const $language = createStore<string | null>(null);
// Let's combine all external stores into a request context
const $ctx = combine({ userId: $userId, language: $language });
// And use it in the request body
const featureFlagsQuery = createJsonQuery({
params: declareParams<{ flagKeys: string[] }>(),
request: {
method: 'POST',
url: 'https://flagr.salo.com/',
body: {
source: $ctx,
fn: ({ flagsKeys }, ctx) => createFlagrRequestBody(flagsKeys, ctx),
},
},
response: {
contract: flagrResponseContract,
},
});
What is createFlagrRequestBody
?
createFlagrRequestBody
is a function that creates a request body for Flagr. If you use another service, you can have to a function that creates a request body for it.
function createFlagrRequestBody(flagKeys, context) {
return {
entities: [
{
entityID: context.userId,
entityContext: context,
},
],
flagKeys,
};
}
Friendly API
INFO
The internal implementation is written just on top level, it will be shared between all createFlag
calls.
So, we have an internal implementation that handles fetching, context passing, etc. Now we need to create a public API that will be available in user-land.
const { $value: $dynamicFaviconEnabled } = createFlag({
key: 'exp-dynamic-favicon',
defaultValue: false,
contract: runtypeContract(Boolean),
fetchOn: applicationInitialized,
});
Let's start with a simple function that registers a new feature:
import { sample } from 'effector';
function createFlag({ key, requestOn }) {
sample({
// every time when requestOn is triggered
clock: requestOn,
// take a key
fn: () => key,
// and register it
target: registerNewKey,
});
}
Now, we have to add a fetching logic:
import { sample } from 'effector';
function createFlag({ key, requestOn }) {
// ...
sample({
// every time when requestOn is triggered
clock: requestOn,
// perform fetching
target: performRequest,
});
}
The last thing we need to do is to return a store with a value of the feature flag:
function createFlag({ key, requestOn }) {
// ...
// find patricular flag
const $value = featureFlagsQuery.$data.map((data) => data.find((flag) => flag.flagKey === key) ?? null);
return { $value };
}
That's it, now let's do some fine-tuning for the createFlag
function.
Default value
Because of ?? null
in the previous example, we will receive null
if the feature flag is not found. It's not what we want, so we have to add a default value:
function createFlag({ key, requestOn, defaultValue }) {
// ...
const $value = featureFlagsQuery.$data.map(
(data) =>
// Use defaultValue if the feature flag is not found
data.find((flag) => flag.flagKey === key) ?? defaultValue
);
return { $value };
}
Validation
The last but not the least thing we have to do is to validate the value of the feature flag. For example, we can receive a string from the server, but we expect a boolean value in our application, so, it will be a runtime error. To prevent it, we can use a Contract
function createFlag({ key, requestOn, defaultValue, contract }) {
// ...
const $value = featureFlagsQuery.$data
.map((data) => data.find((flag) => flag.flagKey === key) ?? defaultValue)
.map((value) => {
// Check if the value is valid
if (contract.isData(value)) {
// if it's valid, return it
return value;
} else {
// otherwise, return a default value
return defaultValue;
}
});
return { $value };
}
Of course, it can be improved a bit. For example, we can use getErrorMessages
method of the Contract to get a list of errors and log them to the console. But it's out of the scope of this article.
Integration
Now, we have a feature flags service. Let's integrate it with our application.
import { createEvent, createEffect, sample } from 'effector';
import { runtypeContract } from '@farfetched/runtypes';
import { Boolean } from 'runtypes';
// Do not forget to call it after application initialization
const applicationInitialized = createEvent();
const { $value: $dynamicFaviconEnabled } = createFlag({
key: 'exp-dynamic-favicon',
defaultValue: false,
contract: runtypeContract(Boolean),
fetchOn: applicationInitialized,
});
const somethingHappened = createEvent();
const changeFaviconFx = createEffect(() => {
const fav = document.querySelector('[rel="icon"][type="image/svg+xml"]');
fav.href = 'other-favicon.svg';
});
sample({
clock: somethingHappened,
filter: $dynamicFaviconEnabled,
target: changeFaviconFx,
});
What else?
That's it, we have a feature flags service. But it's only a part of the story. There are a lot of things that can be improved:
- retry logic to the
featureFlagsQuery
with aretry
operator - caching of the
featureFlagsQuery
with acache
operator - error handing and logging
- request batching
- ...
Conclusion
We have created a feature flags service with Effector and Farfetched. It's not a complete solution, but it's a good start. Key points of this article:
- use Query to fetch data from the remote source
- use Contract because you must not trust remote data
- hide internal implementation details from end users by creating a friendly API and custom factories