Strangler: Building A Feature Flag System In ColdFusion
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.
Learning New Ways to Build-up and Validate Complex Data in ColdFusion
This whole demo took me over a month to build. That's not continuous development time - it's a bunch of mornings here-and-there. But, it certainly took me a lot longer than I had anticipated. This is because the data used to configure a feature flag is much more complex than the data that I normally use in my day-to-day applications.
Where as most of my CRUD (Create, Read, Update, Delete) workflows use simple, flat data that stores nicely in a relational database model, feature flag configuration requires a deeply-nested structure with rules, tests, and roll-outs. Here's the configuration for one of the default feature flags that ships with my Strangler demo:
{
"key": "OPERATIONS--log-level",
"name": "Operations: Log-Level",
"description": "I determine the lowest log-level that is captured during the given user's request.",
"type": "Any",
"variants": [
{
"level": 10,
"name": "TRACE"
},
{
"level": 20,
"name": "DEBUG"
},
{
"level": 30,
"name": "INFO"
},
{
"name": "WARN",
"level": 40
},
{
"level": 50,
"name": "ERROR"
}
],
"rules": [
{
"tests": [
{
"type": "UserKey",
"operation": {
"operator": "OneOf",
"values": [
"1"
]
}
}
],
"rollout": {
"type": "Single",
"variantRef": 1
}
},
{
"tests": [
{
"type": "UserProperty",
"userProperty": "email",
"operation": {
"operator": "EndsWith",
"values": [
"@example.com"
]
}
}
],
"rollout": {
"type": "Multi",
"distribution": [
{
"variantRef": 1,
"percent": 30
},
{
"variantRef": 2,
"percent": 30
},
{
"variantRef": 3,
"percent": 30
},
{
"variantRef": 4,
"percent": 10
},
{
"variantRef": 5,
"percent": 0
}
]
}
}
],
"fallthroughVariantRef": 5,
"isEnabled": true
}
As you can see, this feature flag configuration data is rich with nesting and variation. And, to be honest, I didn't really have a good mental model for how to go about validating and storing this kind of information. Which is why it took me a month-worth of pre-work mornings, finding lots of wrong ways to do it before I settled on an approach that feels "good enough".
The first hurdle I had was how to build up this feature flag data. In a "production" app, I'd have a rich, single-page Angular application (SPA) to manage the UI. But, I didn't want to go on a side-quest about building user interfaces. As such, I developed a way to build-up complex data structures using form POST
s.
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.
Two Different Data Models (or Perhaps Bounded Contexts?)
The other aspect of this that I found challenging was that there was really two different "data models". At first, I tried to have a single set of "feature flag components". But, this quickly created a lot of complexity. I then split the demo in two, creating two completely separate ColdFusion applications:
- Admin - located at
/admin/
in the code. - Demo - located at
/
in the code.
While these two ColdFusion applications share the same JSON data file, they share nothing else. That's because the concept of a "feature flag" is entirely different in these two different contexts. I believe this is what a "bounded context" is within Domain Driven Design (DDD).
In the Admin, the feature flag components build-up, validate, and persist the data. But, they don't process rules or worry at all about targeting. This code has a lot of interesting data management features.
In the Demo, the feature flag components are read-only, assume the data is already valid, and concern themselves entirely with processing rules and targeting users. This code has a lot of interesting data model features.
In the sample repository, these two ColdFusion applications live side-by-side. But, in a production application, these two ColdFusion applications may be completely separated, deployed to different servers, and perhaps even managed by different teams. As such, I think splitting the code in two makes complete sense.
any
and Dynamic Types in ColdFusion
Embracing When I first started building up the "demo" side of this ColdFusion application, I put my academic hat on; I wanted to build components that implemented interfaces and returned typed components. I even had to figure out how to properly 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).
A Taste of the ColdFusion Code
There's a lot of code in this proof-of-concept repository - too much to try and jam into a blog post. So, I'll just show you the main demo page that applies feature flags to sample users.
Feature flags systems don't contain information about users, they contain information about rules. So, just as with the LaunchDarkly targeting, you can 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:
userKey
- which can be used in the "user key tests".userProperties
- which can be used in the "user property tests".
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="/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 />
<#encodeForHtml( user.email )#>
</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.
Reader Comments
Ben the link to demo.png is broken:
www.bennadel.com/resources/uploads/2022/strangler-feature-flag-coldfusion-demo.png
@Rick,
Oh man, not sure how I missed that when I was reviewing 😱 I must have been distracted. I've uploaded the image and purged the CloudFlare cache. Thank you so much for the heads-up!
As part of this POC, I had to think about data validation and normalization in a new way. I've tried to extract those concepts out into their own post:
www.bennadel.com/blog/4308-updated-thoughts-on-validating-data-in-my-service-layer-in-coldfusion.htm
I've already started using this "Validation component" approach in a production app, and I'm really liking it!
In the last month, I've dribbled in some effort to rebuild the Admin UI as an Angular 14 application:
www.bennadel.com/blog/4323-adding-an-angular-14-front-end-to-my-coldfusion-feature-flag-exploration.htm
It still leaves much to be desired, and there's plenty of "best practices" in the Angular that I don't agree with (such as favoring
Promise
s over RxJS. But, it was a lot of fun to build.Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →