Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jonathan Hau and David Bainbridge and Scott Markovits
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jonathan Hau David Bainbridge Scott Markovits

Considering The Ergonomics Of Tags And Objects In ColdFusion

By
Published in Comments (4)

The other day, I had to add an HTTP call from my ColdFusion application to the Postmark API. The results of this API call determine whether or not the current workflow can continue executing; which makes the stability of this API call rather important. In an ideal world, I would have added some retry logic to the CFHttp tag. But, CFHttp doesn't have any in-built retry mechanics. This got me thinking about what it would take to add a retry abstraction to my CFHttp workflow; which, in turn, got me thinking about the overall developer ergonomics of tags and objects in ColdFusion.

I've been writing CFML code for over 20 years now. And, in the early days of ColdFusion, all CFML was tag-based. Modern CFML is predominantly written in script; but, there's no doubt in my mind that early years of tag-based development has built up an immunity to much of the instinctive cringe reactions that other developers will experience when seeing tag-based code.

That said, I can't help but feel that there is something quite elegant looking about some of the tag-based markup. Especially when it is written in a script-based context. To (hopefully) see what I mean, let's look at a simple HTTP call to the aforementioned Postmark API. First, a tag-based version (in CFScript using Lucee CFML):

<cfscript>

	fromDate = "2024-01-01";

	http
		result = "httpResponse"
		method = "GET"
		url = "https://api.postmarkapp.com/stats/outbound"
		getAsBinary = "yes"
		timeout = 5
		{

		httpparam
			type = "header"
			name = "Accept"
			value = "application/json"
		;
		httpparam
			type = "header"
			name = "X-Postmark-Server-Token"
			value = request.postmarkServerKey
		;
		httpparam
			type = "url"
			name = "messagestream"
			value = "benny-test"
		;

		// Child tags can be conditionally executed in the workflow.
		if ( fromDate.len() ) {

			httpparam
				type = "url"
				name = "fromdate"
				value = fromDate
			;

		}
	}

	// HTTP request is IMPLICITLY executed after the closing of the CFHttp tag. The result
	// is saved into the `result` variable assignment.
	dump( httpResponse );

</cfscript>

Again, I know that I'm biased here; but, this looks really nice to me. The nesting of httpparam tags inside the http body clearly illustrates the parent-child relationship. And, the ability to conditionally execute additional httpparam tags inside a nested if-statement looks so clean and easy to read. Frankly, there's a lot to like about this syntax.

Now, let's take the same code and convert it over to using the Http component API rather than tags. The following CFML is doing the exact same thing:

<cfscript>

	fromDate = "2024-01-01";

	httpRequest = new org.lucee.cfml.Http(
		method = "GET",
		url = "https://api.postmarkapp.com/stats/outbound",
		getAsBinary = "yes",
		timeout = 5
	)
	.addParam(
		type = "header",
		name = "Accept",
		value = "application/json"
	)
	.addParam(
		type = "header",
		name = "X-Postmark-Server-Token",
		value = request.postmarkServerKey
	)
	.addParam(
		type = "url",
		name = "messagestream",
		value = "benny-test"
	);

	// Optional parameters can be conditionally added via methods in the workflow.
	if ( fromDate.len() ) {

		httpRequest.addParam(
			type = "url",
			name = "fromdate",
			value = fromDate
		);

	}

	// HTTP request is EXPLICITLY executed on the HTTP object.
	httpResponse = httpRequest
		.send()
		.getPrefix()
	;

	dump( httpResponse );

</cfscript>

The code isn't all that much different. But—to me—the tag-based syntax just feels a bit cleaner, a bit clearer, and a little less noisy. Plus, I love in the tag-based version that the underlying mechanism—the HTTP call—is implicitly invoked once the body of the CFHttp tag is closed. The whole .getPrefix() part of the object-based control flow feels non-intuitive. I mean, what the heck does "prefix" even mean?

Of course, as I said, the code isn't all that much different. It's not like using the object-based syntax is more complicated. It's just different. And just slightly less pleasing.

In this case, the object-based approach is actually just a layer of abstraction over the core CFHttp tag. In fact, if look at Lucee CFML's implementation, you'll see that the org.lucee.cfml.Http code literally turns around and executes the CFHttp tag under the hood.

And, of course, the CFHttp tag is, in and of itself, just a layer of abstraction over the lower-level Java client. Which makes the org.lucee.cfml.Http component a series of nested abstractions.

To bring this back to my original line of inquiry, if I wanted to add retry logic to my HTTP request workflow, I wouldn't be able to alter the native behavior of the CFHttp tag. Instead, I'd have to write my own layer of abstraction that wraps the underlying CFHttp tag; much in the same way that the org.lucee.cfml.Http component does.

And, at that point, I'm probably going to be dealing with objects, not tags. Or, at the very least, dealing with a method-based abstraction, such as makeHttpRequest(), that accepts a similar set of inputs. And, if I'm already dealing with a method-based API, why not just move the whole shebang into a ColdFusion component?

Abstractions are a funny beast. Once you start creating abstractions, it's very tempting to try and be all things to all people. After all, the very nature of abstraction is to simplify execution (via information hiding); which naturally begs the question: how will people want to use this abstraction and how can we make it easy? This consideration can quickly become a slippery slope.

This whole thought started with me consuming the Postmark API. If I wanted to add retry mechanics to the underlying HTTP call, I could:

  1. Create a generic HTTP abstraction with retry logic.

  2. Create a Postmark-specific HTTP abstraction with retry logic.

The former option is very attractive - being able to solve the problem once and then be done with it. But, it's also the hardest option to get right. After all, what makes for meaningful retry logic? Is there a max duration of retries? A perfect back-off formula? Which status codes can be retried? Can the retry logic be extended in user-land?

When you try to solve some of these problems with abstractions, you can quickly end up creating an abstraction that is so flexible and so dynamic that is actually makes the code more complicated to consume.

Better, perhaps, to create a Postmark-specific abstraction that includes just enough retry logic to be helpful for Postmark; but, not so much that the code becomes unwieldy.

But, I'm way off topic now. I started with the ergonomics of tags and objects and now I'm talking about the ergonomics of abstraction layers. It's related but unrelated.

I haven't blogged in a few months due to the writing of my book on feature flags. I'm a bit rusty - give me some time to get back into the swing of things.

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

Reader Comments

198 Comments

Why not just create a generic "retry" wrapper that would retry any block of code any time an exception is thrown?

That way you could use it for any problem. You could have something like:

retry attempts="10" {
// code to retry here

retrycatch {
// something to do when all retry events have finished
}

retryfinally {
// something that should always be applied when the code is finished running
}
}

If you want to catch HTTP errors, you can just check the HTTP status code after your event and if you do not get the expected results, just trigger a throw() to create an error.

So the retry logic would just wrap everything in a try/loop/try workflow. The outer try to handle any unknown exceptions and the "retryfinally" logic. The loop to keep retrying until the attempts have been met. And the inner try to handle the exceptions thrown in the body of your code.

I haven't tried this with a tag syntax, so splitting the try between the start/end modes might not work as outlined above. However, I have used this approach with a component and then using a closure to pass in the body of the logic I want to retry.

15,902 Comments

@Dan,

A generic retry wrapper is interesting. I've tried something similar when I was having trouble with some MongoDB flakiness where I would execute my Mongo SELECT inside a retry:

var results = mongoRetryService.run(
	( mongoClient ) => {

		return( mongoClient.findOne() );

	}
);

This was relatively easy since Mongo queries basically either work or they break - there's very little in between.

The challenge with an HTTP call to an external API is that it becomes much trickier to determine whether or not an API call can be retried (and might even require looking at the response headers, such as with a 429 Too Many Requests response). Plus, there is usually some exponential back-off in between retries.

This is all doable. But, I'm not sure that solving it in a generic way is the best approach. Unless you did something like allow a "retry policy" to be passed into CFHttp. Like:

http ... retryPolicy=shouldRetry;

function shouldRetry(
	any httpResponse,
	numeric retryAttempt
	) {

	return({
		retry: true,  // Do the retry.
		wait: 2000 // 2-seconds.
	});
}

The AWS SDK has something to this effect where you can override the default retry logic that they have implemented for their own API.

Would be interesting to see that actually. I know the Lucee team has a number of similar innovations in recent releases where you can pass a "Listener" component into things like the Query tag.

1 Comments

An automatic retry sounds like a good way to clog up error logging and DDoS the receiver. :)
You'd want to at least check for a recoverable response code, like a momentary outage. If you're retrying a 404 over and over, or a 501 with a malformed payload you're just banging your head on the wall.

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