Using The LaunchDarkly Feature Flag Java SDK With Lucee CFML 5.3.8.201
A few days ago on Twitter, Mary Jo Sminkey asked me if consuming the LaunchDarkly feature flag Java SDK in ColdFusion was as cumbersome as it appeared to be in my post from 2015. That post used the JavaLoader
library and had to jump through some funky thread-management hoops in Adobe ColdFusion 10. Fast forward 6-years, InVision is now running on Lucee CFML 5.3. As such, I wanted to revisit what using the LaunchDarkly Java SDK would look like on a modern ColdFusion CFML platform. Thankfully, it's now hella easy!
View this code in my LaunchDarkly Lucee CFML project on GitHub.
One of the coolest features of the Lucee CFML application runtime is its ability to load Java classes on-the-fly from a given set of JAR files / directories. This makes consuming 3rd-party libraries so easy even a caveman could do it. When using Lucee CFML, all I have to do is download the LaunchDarkly Java SDK JAR files and then reference them into my ColdFusion application when invoking the createObject()
function.
That said, since not all ColdFusion runtimes have this seamless ability to load JAR files via createObject()
, I wanted to isolate the actual class-loading into its own ColdFusion component. This way, if you did want to use some other means, this component would create a flexible abstraction:
component
output = false
hint = "I provide class loading methods for the LaunchDarkly server-side SDK."
{
/**
* I initialize the class loader proxy.
*/
public void function init() {
// NOTE: One of the coolest features of Lucee CFML is the fact that it can create
// Java objects on-the-fly from a given set of JAR files and directories. I mean,
// how awesome is that?! These JAR files were downloaded from Maven:
// --
// https://mvnrepository.com/artifact/com.launchdarkly/launchdarkly-java-server-sdk/5.6.2
variables.jarPaths = [ expandPath( "/vendor/launchdarkly-5.6.2/" ) ];
}
// ---
// PUBLIC METHODS.
// ---
/**
* I load the given class out of the local LaunchDarkly JAR paths.
*/
public any function load( required string className ) {
return( createObject( "java", className, jarPaths ) );
}
}
This ColdFusion component provides a single public method, load()
, which takes the name of the Java Class to return. In this case, I'm using the given set of JAR paths; however, if you were to drop that JAR file(s) into your ColdFusion server context, you could just use a vanilla version of createObject()
to accomplish this same thing quietly behind the component abstraction.
Once I had my Java Class Loader abstraction, I then went about creating a Feature Flag service. The Feature Flag service defines the set of feature flags being consumed within the ColdFusion application (which will almost certainly be fewer than the ones defined in the LaunchDarkly dashboard); and, provides a set of default values for said feature flags should the in-memory client not be able to communicate with the remote LaunchDarkly streaming service.
Back in 2015, in my first LaunchDarkly post, I only knew about Boolean Variations. That is, feature flags that represented a true
/ false
dichotomy. In the years since that post, LaunchDarkly has broadened its concept of "variations" to allow for more robust data types:
- Boolean
- Integer
- Double
- String
- JSON (JavaScript Object Notation)
Now, instead of having just two variations (true
and false
), the other data types allow for an open-ended number of variations. And, with the addition of the JSON variation, you can basically jam whatever data you want into the LaunchDarkly system.
To facilitate the use of all these different data-types in my feature flag variations, I had to re-think how I define them within my ColdFusion code. When invoking the LaunchDarkly Java SDK, you have to have several pieces of information:
- The type-specific method to invoke (on the LaunchDarkly client).
- The feature flag key.
- The unique user key.
- Additional user properties.
- A default feature variation value.
To keep this as simple as possible, I ended up with a configuration object that breaks the feature flags down by type where each type is a ColdFusion Struct in which the "key" is the "feature flag key" and the "value" is the default value provided to the client:
// When we check feature flag state against the LaunchDarkly client, we have to
// call a type-aware method and provide a type-aware default value. As such, we
// need to know the type of each feature flag key. To make this easier to define,
// we'll create type-buckets which will then get collated as a single collection.
// Within each of these buckets, the Struct key is the "feature flag key" and the
// Struct value is the "default variation" to be used if there is a problem
// communicating with the remote LaunchDarkly servers.
variables.featureFlags = collateFeatureFlags({
bool: {
"demo-bool-variation": false
},
double: {
// No double-variation feature flags at this time.
},
int: {
// No int-variation feature flags at this time.
},
json: {
"demo-json-variation": buildLdValue({
which: "first"
})
},
string: {
"demo-string-variation": "first"
}
});
This object makes it easy for the developers to configure each feature flag and provide a default value. The collateFeatureFlags()
method then takes this type-based bucketing and aggregates it into a single Struct that has a set of properties that I can use internally.
Once the LaunchDarkly feature flags are defined, getting the state of feature flags (ie, the variations) for a given user requires us to pass-in all the data needed for targeting. I like to think of the feature flag evaluation process like a Pure Function: all inputs need to be defined explicitly in order for the LaunchDarkly rules engine to calculate the right result.
As such, when you ask for the state of a given feature flag, you have to provide the user key (the unique identifier of the requesting client) and, optionally, a set of custom properties. These properties can then be used for targeting within the LaunchDarkly dashboard. Since these properties are so open-ended, I've decided to use an array of Structs that have name
and value
properties.
On the LDUser
Java Class, there are some setter / getter methods for "standard" custom properties like email
, name
, and ip
(IP address). But, for everything else, there's a set of generic custom()
accessors. In order to siphon the right properties into the right setters, I'm just using a simple switch
block:
/**
* I build the LDUser instance for the given key / primary identifier and supporting
* properties.
*/
private any function buildLdUser(
required string userKey,
required array userProperties
) {
// NOTE: Since the Builder class is a property of the LDUser class, we have to use
// the special internal class "$" notation to access it.
var ldUser = classLoader
.load( "com.launchdarkly.sdk.LDUser$Builder" )
// Key that uniquely-identifies the user / request / client. This value
// should always be a String value. Casting to allow for calling context to
// use numeric values (such as database auto-incrementing columns).
.init( javaCast( "string", userKey ) )
;
for ( var userProperty in userProperties ) {
// NOTE: There are other LDUser customer property methods available (such as
// "private" methods for keeping data local). However, those aren't needed
// for the applications I build - your mileage may vary.
switch ( userProperty.name ) {
case "country":
ldUser.country( userProperty.value );
break;
case "email":
ldUser.email( userProperty.value );
break;
case "firstName":
ldUser.firstName( userProperty.value );
break;
case "ip":
case "ipAddress":
ldUser.ip( userProperty.value );
break;
case "lastName":
ldUser.lastName( userProperty.value );
break;
case "name":
ldUser.name( userProperty.value );
break;
default:
ldUser.custom( userProperty.name, buildLDValue( userProperty.value ) );
break;
}
}
return( ldUser.build() );
}
The resultant LDUser
class instance, returned from this buildLdUser()
method, is then passed into the LaunchDarkly client when evaluating a given feature flag:
/**
* I evaluate and return the targeted variation for the given feature flag and the
* given user (with the given set of custom properties).
*/
public any function getFeature(
required string featureKey,
required string userKey,
array userProperties = []
) {
var ldUser = buildLdUser( userKey, userProperties );
var featureFlag = featureFlags[ featureKey ];
return( getVariation( featureFlag, ldUser ) );
}
The return type of this function is any
because it could literally be anything. The LaunchDarkly feature flags are so flexible, especially with the JSON variations, that you just have to know which type of data to expect based on the type of feature flag you are evaluating. Luckily, ColdFusion is - itself - so freaking flexible that handling this dynamic data is a piece of delicious cake.
With all that said, here's the entirety of my FeatureFlags.cfc
ColdFusion component - note that its constructor takes the class-loader from above and the LaunchDarkly API Key:
component
output = false
hint = "I provide service methods for evaluating feature flag state for users."
{
/**
* I initialize the feature flag service with the given class-loader.
*/
public void function init(
required any classLoader,
required string sdkKey
) {
variables.classLoader = arguments.classLoader;
variables.sdkKey = arguments.sdkKey;
// The LaunchDarkly client should be instantiated and stored as a single, shared
// instance within your application. It will maintain a set of in-memory rules
// that instantly synchronize with any changes made to the remote LaunchDarkly
// dashboard in the background.
variables.ldClient = classLoader
.load( "com.launchdarkly.sdk.server.LDClient" )
.init( sdkKey )
;
// When we check feature flag state against the LaunchDarkly client, we have to
// call a type-aware method and provide a type-aware default value. As such, we
// need to know the type of each feature flag key. To make this easier to define,
// we'll create type-buckets which will then get collated as a single collection.
// Within each of these buckets, the Struct key is the "feature flag key" and the
// Struct value is the "default variation" to be used if there is a problem
// communicating with the remote LaunchDarkly servers.
variables.featureFlags = collateFeatureFlags({
bool: {
"demo-bool-variation": false
},
double: {
// No double-variation feature flags at this time.
},
int: {
// No int-variation feature flags at this time.
},
json: {
"demo-json-variation": buildLdValue({
which: "first"
})
},
string: {
"demo-string-variation": "first"
}
});
}
// ---
// PUBLIC METHODS.
// ---
/**
* I evaluate and return the targeted variation for the given feature flag and the
* given user (with the given set of custom properties).
*/
public any function getFeature(
required string featureKey,
required string userKey,
array userProperties = []
) {
var ldUser = buildLdUser( userKey, userProperties );
var featureFlag = featureFlags[ featureKey ];
return( getVariation( featureFlag, ldUser ) );
}
/**
* I evaluate and return the targeted variation for all of the feature flags and the
* given user (with the given set of custom properties).
*/
public struct function getFeatures(
required string userKey,
array userProperties = []
) {
var ldUser = buildLdUser( userKey, userProperties );
var features = featureFlags.map(
( featureKey, featureFlag ) => {
return( getVariation( featureFlag, ldUser ) );
}
);
return( features );
}
/**
* I identify the user within the LaunchDarkly system. Returns the feature flag
* variations for the given user.
*
* NOTE: This is really just an alias for the getFeatures() method. In server-side
* SDKs, the only impact of identifying users is that they are indexed in the
* LaunchDarkly service. However, in most applications this is not needed because
* users are automatically indexed when used for flag evaluation.
*/
public struct function identifyUser(
required string userKey,
array userProperties = []
) {
return( getFeatures( userKey, userProperties ) );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I build the LDUser instance for the given key / primary identifier and supporting
* properties.
*/
private any function buildLdUser(
required string userKey,
required array userProperties
) {
// NOTE: Since the Builder class is a property of the LDUser class, we have to use
// the special internal class "$" notation to access it.
var ldUser = classLoader
.load( "com.launchdarkly.sdk.LDUser$Builder" )
// Key that uniquely-identifies the user / request / client. This value
// should always be a String value. Casting to allow for calling context to
// use numeric values (such as database auto-incrementing columns).
.init( javaCast( "string", userKey ) )
;
for ( var userProperty in userProperties ) {
// NOTE: There are other LDUser customer property methods available (such as
// "private" methods for keeping data local). However, those aren't needed
// for the applications I build - your mileage may vary.
switch ( userProperty.name ) {
case "country":
ldUser.country( userProperty.value );
break;
case "email":
ldUser.email( userProperty.value );
break;
case "firstName":
ldUser.firstName( userProperty.value );
break;
case "ip":
case "ipAddress":
ldUser.ip( userProperty.value );
break;
case "lastName":
ldUser.lastName( userProperty.value );
break;
case "name":
ldUser.name( userProperty.value );
break;
default:
ldUser.custom( userProperty.name, buildLDValue( userProperty.value ) );
break;
}
}
return( ldUser.build() );
}
/**
* I build an LDValue instance for the given ColdFusion value.
*/
private any function buildLdValue( required any value ) {
return( classLoader.load( "com.launchdarkly.sdk.LDValue" ).parse( serializeJson( value ) ) );
}
/**
* I collate the various types into a single Struct of feature flags with an embedded
* "type" property.
*/
private struct function collateFeatureFlags( required struct featureFlagsByType ) {
var collation = {};
for ( var type in featureFlagsByType ) {
loop
key = "local.featureKey"
value = "local.defaultValue"
struct = featureFlagsByType[ type ]
{
collation[ featureKey ] = {
type: type,
featureKey: featureKey,
defaultValue: defaultValue
};
}
}
return( collation );
}
/**
* I evaluate the given feature flag, extracting the variation targeted for the given
* LanchDarkly user.
*/
private any function getVariation(
required struct featureFlag,
required any ldUser
) {
var featureType = featureFlag.type;
var featureKey = featureFlag.featureKey;
var defaultValue = featureFlag.defaultValue;
switch ( featureType ) {
case "bool":
return( ldClient.boolVariation( featureKey, ldUser, defaultValue ) );
break;
case "double":
return( ldClient.doubleVariation( featureKey, ldUser, defaultValue ) );
break;
case "int":
return( ldClient.intVariation( featureKey, ldUser, defaultValue ) );
break;
case "json":
return( deserializeJson( ldClient.jsonValueVariation( featureKey, ldUser, defaultValue ) ) );
break;
case "string":
return( ldClient.stringVariation( featureKey, ldUser, defaultValue ) );
break;
}
}
}
To test this LaunchDarkly ColdFusion wrapper, I created a simple index.cfm
file and loaded all the feature flags for a made-up user with a given set of custom properties:
<cfscript>
// Get all of the feature flag variations targeted to the given user (with the given
// set of custom properties).
// --
// NOTE: The LaunchDarkly client acts as RULES ENGINE that synchronizes all of the
// targeting information defined in the remote dashboard with its in-memory data
// structures. As such, in order to target a user based on a set of properties, you
// MUST PROVIDE THOSE PROPERTIES at the time you evaluate the feature flags in the
// application code. Think of this process as invoking a PURE FUNCTION - all inputs
// must be provided to the variation evaluation algorithm.
features = application.featureFlags.getFeatures(
userKey = "user-12345",
userProperties = [
{
name: "name",
value: "Ben Nadel"
},
{
name: "role",
value: "admin"
},
{
name: "favoriteMovies",
value: [ "Terminator 2", "Running Man", "Twins", "True Lies" ]
}
]
);
</cfscript>
<cfoutput>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
Using The LaunchDarkly Feature Flag Java SDK With Lucee CFML 5
</title>
</head>
<body>
<h1>
Using The LaunchDarkly Feature Flag Java SDK With Lucee CFML 5
</h1>
<p>
Here are the feature flags targeted at the current user as of
#now().timeFormat( "HH:mm:ss TT" )#:
</p>
<cfdump
var="#features#"
label="Feature Flags"
/>
</body>
</html>
</cfoutput>
Remember, in the LaunchDarkly dashboard, we can use both the user "key" and the "custom properties" to drive the targeting. So, for example, we can go into the Bool
feature flag and turn it on only for users whose set of favorite movies (custom property) contains Terminator 2
:
And, we can go into the JSON
feature flag and turn it on only for users whose role is set to admin
:
Which is why, when we run our demo ColdFusion index.cfm
file we get the following output:
With Lucee CFML, consuming the LaunchDarkly Java SDK in a ColdFusion application is fairly effortless. And, just look at how powerful it is - we're dynamically loading custom JSON payloads into the runtime using the LaunchDarkly dashboard. You can start to see how, if you squint hard enough, the LaunchDarkly dashboard could act as a light-weight "administrative system" for your application.
Feature flags are the cat's pajamas!
If you want to learn more about the awesome power of Feature Flags, check out my Video Presentation: Feature flags Change Everything About Product Development. That video covers much of what I wrote about (and more) in my Feature Flag best practices article.
Want to use code from this post? Check out the license.
Reader Comments
This is great way of loading Java external libraries in isolation on an existing Lucee server. Do you know if this is possible in Adobe Coldfusion or is it only for Lucee ? We are creating a spring boot (none web) API which we want to use on our CF 2018 server but, obviously there will be clashes (eg hibernate) when the tomcat class loader starts loading up the class files.
@Alex,
In an Adobe ColdFusion context, I use the Java Loader component. It's a user-space project:
https://github.com/markmandel/JavaLoader
It's basically the same thing - you give it an Array of
.jar
files and then you can create isolated instances of classes provided by those JAR files.I cannot thank you enough Ben! Been following your blog for years and really appreciate all the help you have given me. ❤️
@Alex,
Thank you for the kind words 😊 Always happy to help out in whatever way I can.
@Alex,
One minor point, you might run into a small security issue. the
JavaLoader.cfc
needs access to the ColdFusion internal libs in order to run. This is a checkbox in the ColdFusion Admin:www.bennadel.com/blog/3699-javaloader-needs-access-to-internal-coldfusion-java-components-in-coldfusion-2018.htm