Blog

Luis Majano

September 04, 2026

Spread the word


Share your thoughts

BoxLang has always been an extensible language. As of 1.17.0, it is a hierarchically extensible one.

Every module ecosystem you have worked in is flat. You declare a list of dependencies and something outside the language, a package manager or a build tool, resolves and downloads and orders them. The language itself has no opinion about the shape of the graph. It sees a list, not a tree.

BoxLang 1.17.0 changes that. Modules can now contain other modules. Recursively. To any depth. Each nested module gets its own class loader, chained to its parent's, and BoxLang's own ModuleService discovers, registers, activates, and unloads that entire tree itself. No package manager. No build tool. No install ordering. Drop in one artifact and everything it depends on comes with it, already wired up and already isolated.

The Convention

Drop a modules/ folder into your module, and BoxLang treats it exactly like a top-level modulesDirectory.

A module folder tree showing the difference between libs/ and modules/

The practical outcome: one self-contained artifact, a single module folder or a single JAR, that brings every module it depends on along with it, fully isolated from the host application's own modules, working the moment it is dropped in.

The distinction that matters most is the one on the right of that diagram, and it is the thing people get wrong first. libs/ and modules/ are not the same thing.

A JAR in libs/ is a library. It is added to your module's classpath and that is the end of its story. No lifecycle, no settings, no class loader of its own.

A JAR in modules/ is a module. It gets its own lifecycle, its own settings, and its own class loader.

Load Order

Nested modules load from the inside out and unload from the outside in.

Nested modules load from the inside out and unload from the outside in

Registering or activating a parent cascades down the whole tree first, so by the time your module's onLoad() runs, everything it bundles is already active and usable. You can call into a nested module from your own startup code without defensive checks, because it is guaranteed to be there.

Unloading reverses it, because a child's class loader depends on its parent's. Children come down before the loader they hang from.

The same logic explains a behavior worth internalizing: disabling a module skips every module nested inside it. A child's loader chains to its parent's, so it cannot load without it.

Class Loader Chaining

Isolation here is real, not conventional. The class loader hierarchy mirrors the module hierarchy: a nested module's loader is parented to its parent module's loader, chaining upward until a top-level module, whose loader is parented to the runtime.

The class loader hierarchy mirrors the module hierarchy

Put a shared dependency in the parent's libs/ once and every module nested inside it can use it without redeclaring anything, while remaining invisible to siblings and unrelated modules. Dependency sharing downward, isolation sideways.

One implementation detail worth knowing if you are working from Java: module class loaders create a real isolation boundary, so they pass null as the standard ClassLoader parent and track the real one themselves. Walk the chain with DynamicClassLoader.getDynamicParent(), because the standard getParent() returns null for a module loader.

A Module Can Be a Single JAR

The other half of Inception is that a bare *.jar sitting in a modules/ folder is a module. No ModuleConfig.bx, no box.json, no surrounding folder at all.

Its descriptor is a Java IModuleConfig registered through META-INF/services/ortus.boxlang.runtime.modules.IModuleConfig, and its metadata comes from a @BoxModule annotation.

@BoxModule(
    name        = "javaHelper",
    version     = "1.0.0",
    author      = "Ortus Solutions",
    description = "A module shipped as a single JAR"
)
public class JavaHelperModule implements IModuleConfig {

    @Override
    public void configure( IBoxContext context, ModuleRecord moduleRecord ) {
        moduleRecord.settings.put( Key.of( "mode" ), "fast" );
    }

}

Without name, a JAR module is named after the JAR file itself, so javaHelper.jar becomes javaHelper. Declaring name lets the module name stand independent of the filename, which matters when your build stamps versions into JAR names.

A JAR that exposes no IModuleConfig is disabled with a warning rather than failing the runtime, so a stray JAR dropped into a modules folder logs a complaint instead of breaking startup.

This is what makes bundled dependencies practical. A pure-Java library your module needs can ship as a first-class module in its own right, without a .bx file anywhere in sight.

A Parent Can Override What It Bundles

Nesting a module does not mean losing control over it. A parent declares a this.modules struct mirroring the boxlang.json shape.

// myModule/ModuleConfig.bx
class {

	this.version = "1.0.0"

	/**
	 * Per-child overrides for the modules nested inside this one
	 */
	this.modules = {
		"childModule" : {
			enabled  : true,
			settings : {
				timeout  : 60,
				endpoint : "https://internal.example.com"
			}
		}
	}

}

The Java equivalent, for JAR modules:

@Override
public IStruct modules() {
    IStruct childSettings = new Struct();
    childSettings.put( Key.of( "timeout" ), 60 );
    childSettings.put( Key.of( "endpoint" ), "https://internal.example.com" );

    IStruct childOverrides = new Struct();
    childOverrides.put( Key.enabled, true );
    childOverrides.put( Key.settings, childSettings );

    IStruct overrides = new Struct();
    overrides.put( Key.of( "childModule" ), childOverrides );
    return overrides;
}

Those overrides land in the middle of a three-layer precedence chain.

Three layer settings precedence: child defaults, parent overrides, global config

The global app config is applied last and always wins, so a deployment can override anything, including a parent's decision about its own child. The merge is additive: settings the parent does not mention keep the child's own defaults. enabled follows the same order, so a parent can switch a bundled module off and the global config can switch it right back on.

Walking the Tree

Nested modules live in the same flat registry as everything else, so they stay globally addressable by name and their BIFs, components, and mappings work exactly as they would at the top level. The relationship is recorded on both ModuleRecord objects, so either end can find the other.

MemberOnDescription
nestedModulesparentArray of names of modules nested directly inside this one
getNestedModule( Key )parentThe record for one direct child, or null
hasNestedModule( Key )parentWhether a module is a direct child of this one
parentModulechildThe Key of the module this one is nested inside, or null
isJarModule()eitherWhether this module is packaged as a single JAR
// From a parent module's ModuleConfig.bx
var child = moduleRecord.getNestedModule( createObject( "java", "ortus.boxlang.runtime.scopes.Key" ).of( "childModule" ) )
log.info( "Child version: #child.version#" )

To see the hierarchy from outside any one module's own code, from a script, an admin dashboard, or a debug session, the new getModuleTree() BIF returns every top-level module as a struct, and every node carries a children struct of the modules nested inside it, recursively.

tree = getModuleTree()

for ( moduleName in tree ) {
	node = tree[ moduleName ]
	writeOutput( "#moduleName# (v#node.version#)" )
	for ( childName in node.children ) {
		writeOutput( "  ↳ #childName#" )
	}
}

Pass a module name to get the subtree rooted at that module instead of the whole forest:

subtree = getModuleTree( "myModule" )
// subtree.children holds myModule's direct nested modules, each with its own .children

An unregistered module name returns an empty struct rather than throwing. Note that nested modules never appear at the top level of the result, so find them under their parent's children entry. getModuleList() and getModuleInfo() still address every module by name in one flat collection, nested or not. getModuleTree() is the one that shows the shape.

Why This Matters

Module authors no longer have to choose between one giant module that does everything and many small modules the user has to install in the right order. A module can be exactly as granular as makes sense internally while presenting as a single install to the outside world.

Because the class loader isolation is real, nested modules never leak their dependencies into modules that did not ask for them. You can bundle a specific version of a Java library without worrying about what version the host application or some other module happens to be using.

One known limitation to be aware of: shutdown ordering across unrelated modules is not dependency-aware, since unloadAll() visits the registry in unspecified order. Nesting order itself is handled, and a module's children always unload before it does.

Next in This Series

Part 2: boxlang check looks at the new syntax checker, why it never executes the code it validates, and how its JSON output gives an AI agent a way to repair its own hallucinated syntax before anything runs.

Resources

Add Your Comment

Recent Entries

BoxLang 3.4 Blog Series Part I: Gateways, One Interface, Any Platform

BoxLang 3.4 Blog Series Part I: Gateways, One Interface, Any Platform

Every AI agent that touches something real eventually needs a human in the loop. Someone has to approve the delete, confirm the wire transfer, or just say "go ahead" before a tool call runs. The question BoxLang AI 3.4.0 answers is: approve it how? A terminal prompt? A webhook? A Slack button?

Luis Majano
Luis Majano
September 04, 2026
Why Migrate from ColdFusion to BoxLang? 10 Reasons to Modernize Your CFML Applications

Why Migrate from ColdFusion to BoxLang? 10 Reasons to Modernize Your CFML Applications

For organizations running applications built with ColdFusion or CFML, modernization often comes with an uncomfortable question:

“Do we keep investing in the platform we already have, or do we rewrite everything in another language?”

That choice is not as binary as it may seem.

At Into the Box 2026, Ortus Solutions Senior Architect and BoxLang core developer Jon Clausen presented a session titled “Top Ten Reasons to Migrate to Bo...

Cristobal Escobar
Cristobal Escobar
September 04, 2026
Be a rebel, but trust us on this: use ColdBox. You'll thank us later!

Be a rebel, but trust us on this: use ColdBox. You'll thank us later!

CFML developers have always had a bit of a rebellious streak.

That's part of what makes CFML great. It gives you enormous freedom to build applications quickly and structure them almost any way you want.

So yes, you can build a successful ColdFusion application without a framework. Many developers have, and some of those applications have been running for decades.

But trust us on this one:

Use a framework.

And if you're building or maintaini...

Cristobal Escobar
Cristobal Escobar
September 03, 2026