Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Robert Bell
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Robert Bell

Using Cached Components As Scoped Proxies In ColdFusion

By
Published in Comments (1)

One of my secondary reasons for building Big Sexy Poems is that it gives me a real-world context in which to try out new (to me) programming paradigms. And one approach that I've been rather enjoying is the use of cached ColdFusion components as Scoped Proxies. This provides the benefit of request-specific state without the overhead and divergent mechanics of per-request component instantiation.

Big Sexy Poems, like all of my ColdFusion applications, uses Inversion of Control (IoC) with a Dependency Injection (DI) container that manages the caching and wiring-up of ColdFusion components (.cfc). My Injector.cfc is a comparatively lightweight, no-frills DI implementation that deals exclusively with long-lived components. Meaning, it has no mechanics for transient, short-lived instantiations.

For some types of request-specific state, this long-lived nature isn't a problem. For example, an abstraction that deals with cgi scope access or that simplifies HTTP header reading can proxy the low-level request metadata without worrying about state since said state is readonly.

A RequestMetadata.cfc component that provides an abstraction over the cgi scope is request-specific but not dynamic in a meaningful way:

component {

	/**
	* I return the ETag for the given request (or the empty string if none exists).
	*/
	public string function getETag() {

		return cgi.http_if_none_match;

	}

	/**
	* I return the HTTP host.
	*/
	public string function getHost() {

		return cgi.server_name;

	}

}

But some ColdFusion components are both request-specific and dynamic. Take my Router.cfc for example. Part of the router abstraction is a set of methods that work together to "walk" the dot-delimited event parameter, making it easier for the request to invoke the targeted CFML modules.

Here is a truncated version of my root controller. Notice that it calls router.next() within the switch expression and then router.nextTemplate() within the case body:

<cfscript>

	switch ( router.next() ) {
		case "account":
		case "auth":
		case "dev":
		case "go":
		case "marketing":
		case "member":
		case "share":
		case "system":
			cfmodule( template = router.nextTemplate() );
		break;
	}

</cfscript>

The .next() method is shifting the next dot-delimited segment off of the event parameter and returning it to the controller. But, it's also setting an internal state variable that's subsequently consumed in the .nextTemplate() call. To illustrate, for the event parameter:

member.poem.list

... the .next() call would return the next segment:

member

... and the subsequent .nextTemplate() call would return the next CFML module path:

./member/member.cfm

Since this state is both updated and consumed several times within a single request (for nested modules), it has to be initialized once at the top of each request. And for that, these type of cached scoped proxies define a special method: setupRequest().

At the root router of my ColdFusion application, I have to gather all of the relevant scoped proxies and initialize them. To see this more clearly, here's my less truncated root router. You will see that I'm calling .setupRequest() on three different ColdFusion components.

<cfscript>

	// Define properties for dependency-injection.
	requestHelper = request.ioc.get( "core.lib.web.RequestHelper" );
	requestMetadata = request.ioc.get( "core.lib.web.RequestMetadata" );
	router = request.ioc.get( "core.lib.web.Router" );

	// ColdFusion language extensions (global functions).
	include "/core/cfmlx.cfm";

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	// While these components are all cached in the application scope, they all need to
	// operate, in part, on request-scoped variables. As such, we have to initialize the
	// request-scoped variables at the start of each request.
	requestMetadata.setupRequest();
	requestHelper.setupRequest();
	router.setupRequest();

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	switch ( router.next( "marketing" ) ) {
		case "account":
		case "auth":
		case "dev":
		case "go":
		case "marketing":
		case "member":
		case "share":
		case "system":
			cfmodule( template = router.nextTemplate() );
		break;
		default:
			throw( type = "App.Routing.InvalidEvent" );
		break;
	}

</cfscript>

It's important to remember that while I'm setting up request-specific data, the cached ColdFusion component instances are still shared across all requests. Which means that I can't use to the variables scope internally, the way I would with a transient component. Instead, I have to use the request scope to store request-specific state; but, I've settled on creating an internal "variables abstraction" — $variables() — that proxies the request scope in an effort to telegraph my intent of "member state".

To illustrate, here's a truncated version of my Router.cfc. Note that the setupRequest() method initializes the request.$$routerVariables structure for the given request; and then the $variables() method provides access to said structure:

component {

	// Define properties for dependency-injection.
	property name="requestMetadata" ioc:type="core.lib.web.RequestMetadata";

	// ColdFusion language extensions (global functions).
	include "/core/cfmlx.cfm";

	// ---
	// LIFE-CYCLE METHODS.
	// ---

	/**
	* I set up the core request structure used internally by the router.
	*/
	public any function setupRequest( string scriptName = "/index.cfm" ) {

		var event = listToArray( url?.event, "." );

		request.$$routerVariables = {
			scriptName: scriptName,
			event: event,
			queue: duplicate( event ),
			currentSegment: "",
			persistedSearchParams: []
		};

		return this;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I return the next event segment, or the given fallback if there is no next segment.
	*/
	public string function next( string fallback = "" ) {

		var segment = $variables().currentSegment = $variables().queue.isDefined( 1 )
			? $variables().queue.shift()
			: fallback
		;

		return segment;

	}

	/**
	* I return the next relative template path used by the view routing.
	*/
	public string function nextTemplate( boolean nested = true ) {

		return nested
			? "./#segment()#/#segment()#.cfm"
			: "./#segment()#.cfm"
		;

	}

	/**
	* I return the current event segment as of the latest route traversal.
	*/
	public string function segment() {

		return $variables().currentSegment;

	}

	// ---
	// PRIVATE METHODS.
	// ---

	/**
	* I am a convenience method to access internal variables scoped to the request.
	*/
	private struct function $variables() {

		return request.$$routerVariables;

	}

}

The use of $variables() isn't strictly necessary — I could have directly referenced the request.$$routerVariables struct throughout the component. But, I use the $variables() method as a point of indirection to decouple the underlying key; and as a means to express the intent of mimicking a more traditional variable scope. Essentially, I'm saying to the developer (future me) that this bag of data is the "variables scope" but tied to the current request.

The huge benefit of this approach is that I can maintain uniformity in my dependency injection — it's nothing but long-lived components. This simplifies the mechanics of the application's inversion of control without giving up the very real need to have request-scoped state.

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

Reader Comments

16,169 Comments

At first, I thought maybe this was the "Flyweight Pattern"; which I was excited about because I don't think I've ever used it by name. But I asked Claude Code if this matches the pattern and it gave me some nuance:

Classic Flyweight: A shared object separates intrinsic state (stored in the object, shared) from extrinsic state (context-dependent, supplied externally). The object is reused across many contexts, but the data it operates on changes.

Your pattern: The component is the shared/intrinsic part (its methods, behavior). The request scope is the extrinsic state - it changes per-request while the component stays cached.

The one nuance: in the textbook Flyweight, extrinsic state is typically passed as method arguments. In your case, the extrinsic state is ambient - it's implicitly available via the request scope rather than explicitly passed in. The setupRequest() method seeds that ambient state, and then all subsequent method calls read from it.

So it was close - same ballpark of design patterns; but, just a little different.

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
Managed ColdFusion hosting services provided by:
xByte Cloud Logo