Building A Feature Flag System In ColdFusion

Cyberdime
Published: August 13, 2022

For the last month-or-so, I’ve been quiet on this blog. Much of that is, unfortunately, stress-related; but, much of it is also do to a small rabbit-hole that I fell into: Feature Flags. If you’ve followed this blog for any period of time, you’ve no doubt seen me rave about feature flags. At work, I use and love LaunchDarkly; but, LaunchDarkly is too expensive for side-projects (such as this blog). As such, I wanted to see if I could create a LaunchDarkly-inspired feature flag system for my own personal ColdFusion projects. I’m calling this proof-of-concept “Strangler” (as in the Strangler pattern).

View this code in my Strangler project on GitHub.

build-up complex data structures using form POSTs.

Then, once I had the data, I had to find a way to validate and sanitize the data before persisting it to a flat JSON (JavaScript Object Notation) file. Even though this is a demo, I wanted to approach it like it was a real application. Which means, I had to expect a non-zero chance that user-provided content would contain malicious information that had to be blocked.

To do this, I created a Validation component that would test and sanitize data. What this ColdFusion component does it, essentially, deep-copy any user provided information by recreating structures with only the expected keys. This way, if the user tried to sneak in some garbage embedded within their JSON payload, the validation object would simply skip-over it, leaving me with only a predictable data structure using proper key-casing and type casting.

namespace components using per-application mappings. I wanted the code to be “correct”.

In the end, however, this created a lot of noise in the code. It was making my component method signatures look huge and unsightly. And, ultimately, I just didn’t want to deal with it. Now, the demo application works because my ColdFusion components “happen to implement” to the right methods; and, they “happen to return” the right data. And, I’m OK with that.

If I could get the CFImport tag to work, we might be having a different conversation. But the import statements are compile time directives. Which means, any mappings they use have to be defined in the ColdFusion Administrator – not in the per-application mappings. And, honestly, I’m so over having any configuration that is not persisted in the code itself (and therefor in the version control system).

think of feature flag targeting as a pure function – you have to pass-in all the data that you want to use when targeting your users. As such, you’ll see that every call to .getVariant() takes two targeting-related arguments:

If you don’t pass-in a “property” that is consumed by a given feature flag test, targeting simply won’t work for that test; and, the targeting algorithm will continue to “fall through” the rules evaluation. If no rules match, the “fall-through” variation is returned.

And, on top of that, a default value has to be provided with every call as well. This is the value that is returned if the feature flag configuration data can’t be loaded; or, if there is an uncaught exception thrown during the evaluation process.

Here’s the root demo ColdFusion page. The users are hard-coded so that changes to the feature flag configuration can be more predictable for the demo:

<cfscript>
	// These are the demo users that we will be targeting with feature flags.
	users = [
		{ id: 1, name: "Leah Rankin", email: "leah@example.com", role: "admin" },
		{ id: 2, name: "Ayden Dillon", email: "ayden@example.com", role: "manager" },
		{ id: 3, name: "Alisa Lowery", email: "alisa@example.com", role: "designer" },
		{ id: 4, name: "Chante Carver", email: "chante@example.com", role: "manager" },
		{ id: 5, name: "Isla-Mae Villarreal", email: "isla-mae@example.com", role: "designer" },
		{ id: 6, name: "Piper Huff", email: "piper@acme.com", role: "admin" },
		{ id: 7, name: "Josie Pruitt", email: "josie@acme.com", role: "manager" },
		{ id: 8, name: "Tessa Corrigan", email: "tessa@acme.com", role: "designer" },
		{ id: 9, name: "Arya Sheridan", email: "arya@acme.com", role: "designer" },
		{ id: 10, name: "Mihai Sheppard", email: "mihai@acme.com", role: "designer" }
	];
	// These are the feature flags that we have in our demo-table.
	operationsKey = "OPERATIONS--log-level";
	productKey = "product-RAIN-123-cool-feature";
</cfscript>
<cfcontent type="text/html; charset=utf-8" />
<cfoutput>
	<!doctype html>
	<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<link rel="preconnect" href="https://fonts.googleapis.com" />
		<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
		<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" />
		<link rel="stylesheet" type="text/css" href="http://www.bennadel.com/static/main.css" />
	</head>
	<body>
		<h1>
			Feature Flag Demo
		</h1>
		<p>
			The two keys being checked are:
		</p>
		<ul>
			<li>
				<strong>Operations (ANY)</strong>: <code>#operationsKey#</code>
			</li>
			<li>
				<strong>Product (BOOLEAN)</strong>: <code>#productKey#</code>
			</li>
		</ul>
		<table width="100%" border="1" cellspacing="2" cellpadding="5">
		<thead>
			<tr>
				<th> ID </th>
				<th> User </th>
				<th> Role </th>
				<th> Operations </th>
				<th> Product </th>
			</tr>
		</thead>
		<tbody>
			<cfloop item="user" array="#users#">
				<cfscript>
					// NOTE: You always have to provide a DEFAULT value, which will be
					// used if there are any errors either loading the data or consuming
					// the data (such as if the given feature flag doesn't exist).
					// Internally, the getVariant() methods are wrapped in a try/catch and
					// are guaranteed not to error.
					operationsVariant = application.strangler.getVariant(
						featureKey = operationsKey,
						userKey = user.id,
						userProperties = {
							name: user.name,
							email: user.email,
							role: user.role
						},
						defaultValue = { level: 50, name: "ERROR" }
					);
					productVariant = application.strangler.getVariant(
						featureKey = productKey,
						userKey = user.id,
						userProperties = {
							name: user.name,
							email: user.email,
							role: user.role
						},
						defaultValue = false
					);
				</cfscript>
				<tr>
					<td align="center">
						#encodeForHtml( user.id )#
					</td>
					<td>
						<strong>#encodeForHtml( user.name )#</strong><br />
						&lt;#encodeForHtml( user.email )#&gt;
					</td>
					<td align="center">
						#encodeForHtml( user.role )#
					</td>
					<td align="center">
						#encodeForHtml( serializeJson( operationsVariant ) )#
					</td>
					<td align="center">
						#encodeForHtml( serializeJson( productVariant ) )#
					</td>
				</tr>
			</cfloop>
		</tbody>
		</table>
		<!---
			When the Admin updates the feature flag data, it posts a message over to this
			demo frame letting us know about the change. When that happens, let's refresh
			the page automatically in order to bring in the latest targeting rules.
		--->
		<script type="text/javascript">
			window.top.addEventListener(
				"message",
				function handleMessage( event ) {
					if ( event.origin !== window.top.location.origin ) {
						return;
					}
					if ( event.data === "adminReloaded" ) {
						window.self.location.reload();
					}
				}
			);
		</script>
	
	</body>
	</html>
</cfoutput>

Now, if we run this ColdFusion application, update some settings, and render the <frameset>, we get the following output:

It’s hard to understand what is going on here based on a static screenshot. The beauty of feature flags is that the ColdFusion runtime becomes highly dynamic; which, is much better illustrated in a video (see above).

This feature flag exploration was a lot of fun to build in ColdFusion; though, it was definitely very frustrating at times, pushed me way outside my normal coding practices, and forced me to evolve the way I think about handling, validating, and sanitizing data. Now that I have my proof-of-concept down, I have to figure out how I want to go about integrating it into this blog.

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

Source: www.bennadel.com