Skip to main content
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Phill Nacelli
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Phill Nacelli

Viewing The LaunchDarkly Feature Flag Evaluation Process As A Pure Function

By
Published in ,

For the last few years, InVision has been using LaunchDarkly as its feature flag service; and, to be honest, it's completely revolutionized the way that we approach development and deployment. At this point, I can't imagine working without it. But, after almost 4-years of usage, I still see people within our organization getting confused about how the LaunchDarkly client actually applies the rules of user targeting. As such, I want to share the mental model for feature flag evaluation that has worked well for me: Pure Functions.

To be clear, I'm not saying that the LaunchDarkly client uses a Pure Function under the hood - I have no idea how it works. I am only stating that the metaphor of a Pure Function is how I model the feature flag evaluation; and, that it has served me quite well, especially after struggling with my own conceptual hurdles when using LaunchDarkly for the first time.

With that said, a Pure Function is a function whose outputs are calculated based solely on its inputs. That is, a pure function doesn't rely on any state other than the state provided to it. And, the result of the calculation is always the same when given the same set of inputs.

Bringing this into a LaunchDarkly context, feature flag evaluation is like a Pure Function because it determines feature flag status based solely on its inputs: Targeting Rules and User Properties. The Targeting Rules are implicitly provided by the LaunchDarkly service stream; and, the User Properties are explicitly provided by you in the code:

LaunchDarkly feature flag evaluation pure function metaphor.

To see this in action, imagine that we use the Node.js client to identify a user with a robust set of properties:

launchDarklyClient.identify({
	key: "ou812",
	name: "Ben Nadel",
	email: "ben@bennadel.com",
	country: "US",
	custom: {
		groups: [ "admin" ]
	}
});

Doing this will create a new user in the LaunchDarkly dashboard with all the information that we provided to the .identify() call:

LaunchDarkly user identification.

Now, let's create a Targeting Rule for a feature, "admin-tools", that applies to all users that are in the group, "admin":

LaunchDarkly user targeting rules.

When you first start using LaunchDarkly, your mental model for user targeting may be more akin to that of a Relational Database in which you can INNER JOIN many tables together in order to locate related data. As such, you may author your application code to evaluate the user feature flags like this:

var canSeeAdmin = await launchDarklyClient.variation(
	"admin-tools",
	{
		key: "ou812",
		email: "ben@bennadel.com"
	},
	false // Default variation.
);

From a Relational Database perspective, this makes complete sense - you're providing the "primary key" of the user and expecting to be able to locate the full user record which would contain the "group" data. However, if we run the above code, we will see that the feature flag variation evaluates to: False.

The fundamental mistake here is assuming that the LaunchDarkly client has access to the entire set of user-data. It does not. It is just a rules-evaluation engine. And, when reframing the feature flag evaluation as a Pure Function, we can create a mental model that looks like this:

LaunchDarkly variation evaluation as a pure function.

As you can see, with the given inputs, it suddenly becomes clear why the feature flag was evaluated as False: the user Input do not contain a "groups" property. As such, there is no way for the Targeting Rules Input to match against it. The fact that the LaunchDarkly user database happens to know which group the user is in is completely irrelevant - that data is contained within an entirely different, remote system.

Using this Pure Function mental model, we can see that in order to apply the "groups" targeting, we have to provide the "groups" property when evaluating the feature flags for the given user:

var canSeeAdmin = await launchDarklyClient.variation(
	"admin-tools",
	{
		key: "ou812",
		custom: {
			groups: [ "admin" ]
		}
	},
	false // Default variation.
);

Given these inputs, the Pure Function feature flag evaluation will consistently result in: True.

LaunchDarkly variation evaluation as a pure function.

One thing to keep in mind is that the "key" property is always required. Even if your rules are targeting non-key values, the "key" property must be provided to the .variation() method. If it is not provided, the rules evaluation will not work as expected.

To see the codification of this discussion, here is my Node.js code:

// Require the core node modules.
var chalk = require( "chalk" );
var config = require( "./config" );
var LaunchDarkly = require( "ldclient-node" );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

async function runDemo( apiKey ) {

	// NOTE: Internally, the LaunchDarkly client will set up an INTERVAL, which will
	// hold the node.js process open until you explicitly .close() the client. In a
	// long-running application, this doesn't matter. But, in a "test" script, just
	// something be aware of.
	var launchDarklyClient = LaunchDarkly.init( apiKey );

	await launchDarklyClient.waitForInitialization();

	// To better set the context for rule-set evaluation, let's identify the user in
	// a separate step. In a real app, you may never need to call this as the feature
	// evaluation functions will automatically identify users. But, this allows for more
	// robust user information to be provided to the LaunchDarkly dashboard.
	launchDarklyClient.identify({
		key: "ou812",
		name: "Ben Nadel",
		email: "ben@bennadel.com",
		country: "US",
		custom: {
			groups: [ "admin" ]
		}
	});

	// Now that we've identified the user (and subsequently targeted them in our
	// LaunchDarkly dashboard), let's check to see if the user has access to a feature.
	// For this demo, we're going to be targeting the ADMIN GROUP; however, on this first
	// attempt, we're going to provide only "key" and "email".
	var canSeeAdmin = await launchDarklyClient.variation(
		"admin-tools",
		// NOTE: When evaluating a feature, the LaunchDarkly client CAN ONLY USE the
		// properties that YOU PROVIDE RIGHT HERE. The client does not keep a local
		// database of users - it only keeps a copy of the rules defined in the
		// LaunchDarkly targeting interface. As such, it can only run those rules against
		// these provided properties:
		// --
		// NOTE: The "key" is required, or evaluation will be skipped.
		{
			key: "ou812",
			email: "ben@bennadel.com"
		},
		false // Default variation.
	);

	console.log( chalk.cyan( "(1) Can See Admin:", chalk.bold( canSeeAdmin ) ) );


	// Now, let's try evaluating the feature again, this time providing the groups custom
	// property as part of the variation check.
	var canSeeAdmin = await launchDarklyClient.variation(
		"admin-tools",
		{
			key: "ou812",
			custom: {
				groups: [ "admin" ]
			}
		},
		false // Default variation.
	);

	console.log( chalk.cyan( "(2) Can See Admin:", chalk.bold( canSeeAdmin ) ) );

	// NOTE: Only closing the client because this is a demo; otherwise, I would leave
	// the client open and connected to the LaunchDarkly stream.
	launchDarklyClient.close();

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

Promise
	.resolve( config.launchDarkly.apiKey )
	.then( runDemo )
	.catch(
		( error ) => {

			console.log( "Oh noes!" );
			console.error( error );

		}
	)
;

As you can see, it is evaluating the "admin-tools" feature flag twice: once with a user object that lacks the the "groups" property; and, once with a user object that includes the "groups" property. And, when we run this LaunchDarkly test through node.js, we get the following terminal output:

(1) Can See Admin: false
(2) Can See Admin: true

As you can see, the first feature flag variation evaluated to False, the second evaluated to True. This is because only the second invocation of the "Pure Function" rules engine had the necessary Inputs to calculate the desired Output.

LaunchDarkly is a really awesome service. But, there's no doubt that the "rules engine" concept is a hard one to grasp. Especially when you spend most of your day thinking about relational data. Hopefully the metaphor of a "Pure Function" helps you better conceptualize the rules engine; and makes it easier to figure out how to target the right users given the right inputs.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel