Building Resilient Feature Flags That "Fail Open" In ColdFusion
When it comes to feature flags, I'm a super fan! I believe that they fundamentally change everything about product development (See Video Presentation). But, with that super power comes complexity. Feature flags mean branching logic and transient state. And, this complexity is only amplified when you have to reconcile a long-running process, like a Single-Page Application (SPA). One way to help reduce complexity is design resilient feature flags so that they "fail open". This makes it much easier to clean up your ColdFusion server without breaking your client-side code.
If you use feature flags in a traditional multi-page application (MPA), you have less complexity to contend with. This is because every page navigation results in a new process, a new page, a refreshed state. As such, any change to your feature flag configuration is automatically expressed the next time a user does anything in the application.
With a Single-Page Application (SPA), state is persisted in a long-running client-side process. This makes it more challenging to propagate server-side feature flag changes back to the client. One approach that I use to remediate some of this complexity is to return permissions with my API response payloads. This way, when a user is fetching the data for a given User Interface (UI), they will also receive updated permissions for that UI, along with any feature-flag driven state.
To see this in action, let's look at a simulated ColdFusion API response alongside a simulated Single-Page Application (SPA) client. In the following code, the server is constructing an API payload to return to the SPA; and, the SPA is consuming that fake API payload using an Immediately-Invoked Function Expression (IIFE).
Note that the API response includes a featureFlags
structure that contains our transient feature flag state required by the UI. In this case, the Duplicate Contact feature is currently being built behind a feature flag:
<cfscript>
// IMAGINE: This is an API response to a request from the Single-Page Application.
apiResponse = serializeJson({
contact: {
id: 4,
name: "Jo Jo Bananas",
email: "jojo.b@example.com",
role: "Admin"
},
// As part of the response, we are going to include some feature flags that
// determine which features are enabled / visible in the client-side interface.
// Imagine that we are in the middle of building a "duplication" feature. And, we
// only want to show the option to "duplicate" if this feature flag is enabled.
featureFlags: {
canSeeDuplicate: false // User cannot see this feature... yet!
}
});
</cfscript>
<!--- ------------------------------------------------------------------------------ --->
<!--- ------------------------------------------------------------------------------ --->
<!--- Imagine that this is a ColdFusion Server / Single-Page Application (SPA) --->
<!--- divide. And, that the following code is part of a LONG RUNNING PROCESS. --->
<!--- ------------------------------------------------------------------------------ --->
<!--- ------------------------------------------------------------------------------ --->
<!doctype html>
<html lang="en">
<head>
<title>
Single-Page Application (SPA)
</title>
<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body>
<h1 id="contactName">
{{ name }}
</h1>
<p id="contactEmail">
{{ email }}
</p>
<p id="contactRole">
{{ role }}
</p>
<p class="tools">
<a href="./edit.cfm">
Edit Contact
</a>
<a href="./delete.cfm">
Delete Contact
</a>
<!---
Note that the "duplicate" feature DEFAULTS to being HIDDEN (display: none).
It will only be turned-on if the feature flag is enabled.
--->
<a id="contactDuplicate" href="./duplicate.cfm" style=" display: none ; ">
Duplicate Contact
</a>
</p>
<!--- IMAGINE: Controller for the Single-Page Application (SPA) view. --->
<script type="text/javascript">
(function applyApiResponse( response ) {
// Reconcile the view with the API response.
contactName.textContent = response.contact.name;
contactEmail.textContent = response.contact.email;
contactRole.textContent = response.contact.role;
// If the feature flag is TRUE(thy), show the duplicate button.
if ( response.featureFlags.canSeeDuplicate ) {
contactDuplicate.style.display = "block";
}
})(JSON.parse( "<cfoutput>#encodeForJavaScript( apiResponse )#</cfoutput>" ));
</script>
</body>
</html>
If we run this ColdFusion code, we get the following output:
Notice that the UI has no "Duplicate Contact" action. This is because the feature flag for canSeeDuplicate
is set to false
; and, the API response handler is using that value to enable the duplicate functionality in the client.
This is great! This is the power of feature flags! This is why they have revolutionized product development.
Now, imagine that we want to enable this feature for a given set of users. All we would need to do is go into our feature flag dashboard and enable targeting for those users. Then, the next time they make this API call, they'll receive the updated feature flag configuration.
In this demo, we simulate this experience by hard-coding true
into our API response:
<cfscript>
apiResponse = serializeJson({
// ... truncated ....
featureFlags: {
canSeeDuplicate: true // This feature is now ENABLED for this user!
}
});
</cfscript>
Now, if we refresh this ColdFusion page - simulating an API response from the Single-Page Application (SPA) - we get the following output:
As you can see, once we enabled the feature flag and the API response propagated the feature flag state, the Single-Page Application renders the "Duplicate Contact" button.
Again, this is the power of feature flags! This is why we love them!
But, getting a feature flag into the application is only half the battle. Once a feature has been fully adopted, we must remove the feature flag from the application - this is a mandatory best practice. Leaving unnecessary feature flags in your code creates unnecessary complexity, is more likely to lead to bugs, and is generally a conduit for "code rot".
When building a Multi-Page Application (MPA), this is easy! Delete the feature flag branching / logic and the user will see the "right" state the next time the navigate to the page. With a Single-Page Application (SPA), however, the story isn't so simple. Remember, a SPA represents a long-running process on the client-side. It might be days or weeks before a user refreshes their browser and downloads the latest SPA bundle.
Let's see what happens with your simulated application if we try to remove the feature flag from the ColdFusion code, but leave it in on the client-side to simulate the long-running process:
<cfscript>
apiResponse = serializeJson({
// ... truncated ....
featureFlags: {
// ... the concept of this flag has been removed from the server ...
}
});
</cfscript>
As you can see, we've removed the canSeeDuplicate
feature flag from our API response. And now, when the user returns to our simulated User Interface, they see this:
Oh no! The "Duplicate Contact" feature is gone, despite the fact that we intended to "commit" to this feature.
The problem lies in the way in which we coded the client-side logic to consume the feature flag state. Let's take a closer look at our if
statement:
// If the feature flag is TRUE(thy), show the duplicate button.
if ( response.featureFlags.canSeeDuplicate ) {
contactDuplicate.style.display = "block";
}
This if
statement consumes the feature flag state as a Truthy value. Meaning, the "duplicate" feature is never expressed in the client-side UI if the feature flag is a Falsy value. false
is a falsey value. But, so is undefined
. And, when we removed the canSeeDuplicate
key from our API response, the client started evaluation the response.featureFlags.canSeeDuplicate
expression as undefined
; ergo falsy; ergo, the feature remains hidden.
I call this "Failing closed". When the feature flag was in an "undefined" state on the client-side, the application falls-back to keeping the feature hidden.
To fix this, we need to code our feature flag consumption so that it "Fails open". Meaning, when the feature flag enters an "undefined" state, we want the feature to be expressed in the client-side UI. For this, all we have to do is update our client-side check to look for an explicit false
value:
// Show the feature any time the flag is not EXPLICITLY FALSE!
if ( response.featureFlags.canSeeDuplicate !== false ) {
contactDuplicate.style.display = "block";
}
Now, if we reload our ColdFusion page, we get the following output:
Woot woot! Our feature is rendered for our user! And, this is despite the fact that the ColdFusion server no longer has any logic pertaining to the feature flag. This is because the client-side code now operates in three distinct states:
.canSeeDuplicate === true
- Explicit on.canSeeDuplicate === false
- Explicit off.canSeeDuplicate === undefined
- Fail open
It's this last state - the "fail open" state - that makes the client-side code resilient to feature flag removal. Now, when your team has committed to a feature, and removes the feature from the server-side code, the client-side code will naturally "fall into place" even if the user doesn't download the latest client-side bundle.
What If I Want To Revert a Feature?
It's one thing to "roll forward" with a feature, committing to it and making it available to all users. And, it's another thing to "roll back" a feature. With the client-side now coded to "fail open", attempting to "roll back" a feature may inadvertently show an undesired Call-To-Action to a user who hasn't refreshed their page in a while.
In such cases, I'll just hard-code the false
value into the API response with a note:
<cfscript>
apiResponse = serializeJson({
featureFlags: {
// 2023-04-14: This feature no longer exists in the application.
// Leaving this response hard-coded so that long-running client
// code doesn't accidentally turn something on. We can delete this
// line in a few months after we know that all clients have likely
// refreshed their client-side bundles.
canSeeDuplicate: false
}
});
</cfscript>
In 99% of cases, feature flag development leads to new features. But, every now and then, you try an experiment that turns out to be a mistake. Thankfully, we can handle that case pretty easily.
Feature flags are the best!
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →