Improving module loader / script architecture

One of the most frequent problems I’ve been having with my project recently is organization, and I find myself revamping it more than actually creating new systems. I’ve been looking into other topics about module loaders, frameworks, and single-script architectures for a while now, and I’ve put together a basic system from that, but I’m still not completely satisfied with it.

Right now I have a basic module loader setup of a single script (in ServerScriptService) and localscript (in StarterPlayerScripts) that loop over all of their modules, require them and add them to a table, then call their Init() and Start() functions. Something like this:

local modules = {}

local function Setup(path)
	for _, module in pairs(path:GetDescendants()) do
		if module:IsA("ModuleScript") then
			modules[module.Name] = require(module)
		end
	end
end

Setup(script)
Setup(ReplicatedStorage)

for _, module in pairs(modules) do
	if module.Init then
		module.Init(modules)
	end
end

for _, module in pairs(modules) do
	if module.Start then
		module.Start()
	end
end

From there, the ModuleScripts usually follow this basic stucture:

local Module = {}

-- Services here

-- Other modules here
local Module1
local Module2

-- Module-specific functions here

-- Init (Optional, generally used if module creates connections or requires other modules)
function Module.Init(modules)
	Module1 = modules.Module1
	Module2 = modules.Module2
end

-- Start (Optional, generally used if module changes other objects)
function Module.Start()

end

return Module

This setup is fully functional and prevents cyclic require() calls, but there are a few points I dislike about it:

  • Every time I want to use another module, I have to add a new variable at the top of the script and set that variable in the Init() function.
  • It doesn’t support autocompleting module functions because it isn’t requiring them directly.
  • I used this to allow modules to require eachother, but I should probably be avoiding that outright.

Additionally, a different pattern I have seen is to store module references in a “core” ModuleScript and have other modules require that instead of using the Init() function as seen above. Would that be a better alternative?

Regardless, I’d like to know if there’s anything I’m doing wrong with this and if anyone more experienced with organizing projects can give advice or examples.

That is because you made the Module static. Instead, you should consider to use instantiated modules. You can also use controllers and services for static logic (see Knit).

It is not easy to find a nice solution for this. Maybe you can steal some ideas from other frameworks like Knit and Matter.

There are existing frameworks (Knit) that support modules and patterns like controllers and services, but writing one yourself can be a fun challenge.

2 Likes