Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Mark Drew
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Mark Drew ( @markdrew )

Considering Control Flow And Transient Data Relationships In ColdFusion

By on
Tags:

Back in the day, when I had no separation of concerns in my ColdFusion application architecture, some things were actually easier because I was always dealing with raw data. Which meant, if I had an optional or transient relationship between two entities in my database, I could query for a record and then simply check .recordCount on the CFQuery results to see if the relationship existed. Now, however, with a layered ColdFusion architecture that boasts a strong separation of concerns, that raw data is abstracted away; and, while many things have become easier, dealing with these transient relationships has become a bit harder. And, I'm still trying to figure out how to best handle this in ColdFusion.

My ColdFusion applications have several different layers. Generally speaking, I refer to these - from the outside in - as:

  • Controller - is the "delivery mechanism" for the application. It handles incoming request and wrangles responses for the user.

  • Workflow - is the orchestration of commands that mutate the system. This is where a bulk of the user-based security is located. It is also where cross-service logic is located.

  • Service - this handles all things related to the integrity of a given entity within the application core. It only knows about the data that it manages; and, doesn't care about referential integrity across entities (that's the Workflow layer's job).

  • Gateway - this handles the low-level data persistence for a given service. In other words, this is the SQL layer (in most of my applications, I use a relational database for storage).

As you go down deeper into this stack, the data gets more raw. So, when the Service layer calls into the Gateway layer, it often - but not always - gets back a CFQuery object. Which means, I can use .recordCount in my Service layer to effortlessly check for data existence in my control flow.

However, as this data is passed up the stack, it is transformed. When the Workflow layer calls into the Service layer, the Service layer doesn't return the underlying CFQuery object, it returns a normalized Data Transfer Object (DTO) - which is really just a fancy term for Struct.

So, what happens then when the Workflow layer needs to consume a Service layer value that may or may not exist? Consider this pseudo-code for a Reminder workflow that sends out a birthday message to a user that may have an optional nickname:

component {

	public void function sendBirthdayReminder( required numeric userID ) {

		var user = userService.getUser( userID );

		// If the user has a nickname that they like to use within the system,
		// let's use that in their birthday email.
		try {

			var nickname = nicknameService.getNicknameForUser( user.id );
			var preferredName = nickname.value;

		} catch ( NotFound error ) {

			var preferredName = user.name;

		}

		emailService.sendBirthdayReminder(
			userID = user.id,
			userEmail = user.email,
			userName = preferredName
		);

	}

}

In this Workflow operation, the .getNicknameForUser() method call either returns the nickname entity for the target user; or, it throws an error. Normally, I have no objection to throwing errors in ColdFusion - it's important that a method throw an error when it cannot uphold its contract. In this case, however, we're throwing errors to manage control flow, which I do not love.

So how might I go about getting rid of this try/catch based control flow? One option is allow the NicknameService.cfc to return a possibly-null value:

component {

	public void function sendBirthdayReminder( required numeric userID ) {

		var user = userService.getUser( userID );
		var nickname = nicknameService.getNicknameForUser( user.id );

		var preferredName = isNull( nickname ) // NULL CHECK.
			? user.name
			: nickname.value
		;

		emailService.sendBirthdayReminder(
			userID = user.id,
			userEmail = user.email,
			userName = preferredName
		);

	}

}

I don't like this approach because the method name, .getNicknameForUser(), now feels like its lying to me. I asked it for a nickname, and it might return a nickname, or might return NULL. At this point, I'd have to start adding null-checks everywhere I call this method. If I wanted to litter my code with all manner of error handling, I might as well start programming in Golang. Which is obviously not the direction we want to Go (pun intended).

Sometimes, I see people handle this type of scenario by having a Service layer return an empty struct when the underlying record doesn't exist:

component {

	public void function sendBirthdayReminder( required numeric userID ) {

		var user = userService.getUser( userID );
		var nickname = nicknameService.getNicknameForUser( user.id );

		var preferredName = structIsEmpty( nickname ) // STRUCT CHECK.
			? user.name
			: nickname.value
		;

		emailService.sendBirthdayReminder(
			userID = user.id,
			userEmail = user.email,
			userName = preferredName
		);

	}

}

To me, this is the worst possible solution since the Service layer is returning the right data type; but, that data is a complete lie. At least when you return NULL, you're being honest about the data type in question; and, the null-reference errors you get will be straightforward (as opposed to the "undefined key" errors that you'll inevitably get when trying to consume an empty Struct later on).

Another option is to return an Array from the Service layer and then check its length:

component {

	public void function sendBirthdayReminder( required numeric userID ) {

		var user = userService.getUser( userID );
		var nickname = nicknameService.getNicknameForUser( user.id );

		var preferredName = arrayLen( nickname ) // ARRAY CHECK.
			? nickname.first().value
			: user.name
		;

		emailService.sendBirthdayReminder(
			userID = user.id,
			userEmail = user.email,
			userName = preferredName
		);

	}

}

This hearkens back to the old days when I was always dealing with CFQuery. Except, instead of dealing with .recordCount, I'm dealing with .len(). This feels better than dealing with NULL or structIsEmpty(); but, it still feels wrong because the method name isn't doing what it says its doing. Meaning, I asked for a Nickname entity and I get an Array? This is almost certainly going to lead to errors.

At this point, I'm starting to see a theme to my problems: The method isn't doing the thing that it says it was doing. So maybe the real problem is the method name.

In other programming realms, there are language facets that represent a thing that may or may not exist. Promises, Futures, Null-Objects, Maybes - you've probably dealt with something along these lines. So maybe this is what I need to be using here.

Of course, I don't want my "get" methods to start returning "Maybes" - at that point, I'd still have to litter my code with results-checking. And, if that's the case, I might as well just start returning NULL and not add the complexity.

The key here is that I need both a meaningful method name and a simple data structure. Consider this Service-layer method for getting a user's nickname:

component {

	public struct function maybeGetNicknameForUser( required numeric userID ) {

		var results = gateway.getNicknamesByFilter( userID = userID );

		if ( results.recordCount ) {

			return({
				exists: true,
				value: asDataTransferObject( results )
			});

		} else {

			return({
				exists: false
			});

		}

	}

}

Notice that this method starts with maybe. This indicates that the return values isn't the Nickname, it's a structure that maybe contains the nickname entity.

Now, in my Workflow layer, I can call this new maybe method:

component {

	public void function sendBirthdayReminder( required numeric userID ) {

		var user = userService.getUser( userID );
		var maybeNickname = nicknameService.maybeGetNicknameForUser( user.id );

		var preferredName = ( maybeNickname.exists )
			? maybeNickname.value.value
			: user.name
		;

		emailService.sendBirthdayReminder(
			userID = user.id,
			userEmail = user.email,
			userName = preferredName
		);

	}

}

All of this code looks more-or-less exactly the same. But, this snippet of code is the first one that feels like it's doing what it says it's doing; and, allows me to gather transient relational data without having to use try/catch based control-flows; and, without having to call back into the Service layer multiples times in order to perform both an existence check followed by a fetch.

ASIDE: In the above code, I briefly considered using the Elvis operator to perform the assignment:

var preferredName = ( maybeNickname.value.value ?: user.name );

... but, I wanted to show the use of .exists. Plus, value.value just reads rather funny, don't you think?

I'm gonna start playing around with this approach in my ColdFusion applications. It's been top-of-mind for me since I'm currently building a control-flow that consumes some transient relational data; so, I'll immediately get a real-world sense of whether or not this feels right.

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

Reader Comments

15,674 Comments

@Chris,

I think that would make sense, perhaps as a private method in the Workflow component. But, you're still going to have to eventually deal with the transient data. Meaning, internally to the getPreferredName() method, you'd still have to have the same logic. At which point, I think the conclusions from this post can still be applied.

But, I do think that factoring-out the logic to its own method would make the original method easier to read.

205 Comments

@Ben

Agreed. The primary benefit I see in this approach is that the logic to deal with the transient data is centralized in one place, so you can do away with the try/catch, null checks, structkey checks, elvis operators, etc which would otherwise be spread throughout the app.

6 Comments

Chris makes a good point about the helper method.

Since I have been on a clean code / clean architecture kick lately, I have more opinions :P Sorry it's long.

I usually think that any time you're applying business logic to an entity's internal structure outside of an entity, you have to wonder where else is this logic being duplicated. If its reasonable to expect it being used elsewhere, a helper is a good option.

With this approach you are encapsulating the logic of what a preferred name is, in the entity, which has the data and the behaviors of that object.

I know you have a service and not necessarily an entity here, but you could still make a helper where you pass the data into it, and get the return you need.

The goal here is have your sendBirthdayReminder function do one thing and one thing only. Get the data and pass to the emailservice. Although your logic is getting data in the right format, it is working on 2 levels of abstraction/detail. It needs to know too much, in my opinion.

To help clean up some of this boiler plate I would abstract further with a memento for my object, the mementifier module allows me to ask for the values I want. It calls getFIELD so if you have a helper function, it will do it for you, without any other code.

You embed the logic for a preferredName in a userObject... or adapt or using queries and mimic mementifier in a base service method instead which can look for a key, or a method if you're not using entity objects.

public void function sendBirthdayReminder( required numeric userID ) {
var user = userService.getUserEntity( userID ).getMemento( includes="id,email,preferredName" );

emailService.sendBirthdayReminder(
		userID = user.id,
		userEmail = user.email,
		userName = preferredName
	);

With GetMemento - I can ask for id, email, preferredName, it will return a struct with id=getID(), email=getEmail, and preferredName=getPreferredName().

15,674 Comments

@Gavin, @Chris,

I think some of the discussion here might be due to me picking an overly simple example. My intent here was that "user" and the "nickname" services weren't supposed to know about each other - and that the "workflow" was the thing responsible for merging the two data-sets together. But, maybe the domain-overlap here is too great and the separation requires too much suspension of disbelief.

But Gavin, to your point, if the "user" service were to own both the user data and the nickname data, then I agree - it seems like the "user" service should just take care of that, and my calling context wouldn't have to worry. I'm not sure exactly what the memento stuff is - that might just be a pattern that I'm not aware of. But, I see that you're just asking it to get data that is presumably computed internally (again, hiding the details from the calling context).

The notion of "do one thing" is so hard to wrap ones head around. It has always felt too open to interpretation. If the "one thing" that the sendBirthdayReminder() does it orchestrate the birthday notification, it "feels OK" (to me) to do some data translation as part of the orchestration. But, like I said, the "do one thing" is one of those phrases that appears to be simple, but is actually not straightforward when you start to think about details.

15,674 Comments

@Gavin,

Also, what are you using for the Clean Code stuff? Are you going through Uncle Bob's Clean Coders stuff? Or you just mean something more general? Wondering if there some juicy stuff I should be checking out?

15,674 Comments

@Brad,

Optional looks interesting. I feel like I'm already giving you some PTSD from your interview of CFAlive where you were ranting about how no one know how much prior art there is in the ColdFusion community 😂

It seems like we eventually kind of get to the same place. Only, I'm using an on-the-fly struct and you've codified it into a more official (and obviously more robust) concept. I'll have to take a closer look at the code.

Post A Comment — I'd Love To Hear From You!

Post a Comment

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