.This is a simple technique I’ve found to completely remove all cyclic module dependencies without compromises.
The technique is simple, and hinges on the fact that values returned by module scripts can be changed. In order to utilise this technique, simply create a second modulescript which only returns a table. We’ll be calling these modulescripts contracts from now on, and the modulescripts with all the functions will be called implementation modules.
Then, when creating an implementation module do not create a new table for that modulescript. Use the table from the relevant contract instead.
Let’s look at an example. We’ll assume we have the following hierarchy:
- ServerScriptService
- Script
- MessageModule
- GreeterModue
-- Script
local Message = require(script.Parent.MessageModule)
Message.Say()
Message.Say()
-- MessageModule
local ServerScriptService = game:GetService("ServerScriptService")
local Greeter = require(ServerScriptService.GreeterModule)
local Message = {}
local message = ""
function Message.SetMessage(msg: string)
message = msg
end
function Message.Say()
Greeter.Say(message)
end
return Message
-- GreeterModule
local ServerScriptService = game:GetService("ServerScriptService")
local Message = require(ServerScriptService.MessageModule)
local Greeter = {}
function Greeter.Say(msg: string)
print(msg)
Message.SetMessage(msg .. " (1)")
end
return Greeter
This will obviously cause a cyclic module dependency error, because MessageModule requires GreeterModule which requires MessageModule. Now let’s implement this technique.
Our new hierarchy will look like this:
- ServerScriptService
- Script
- MessageModule
- GreeterModule
- MessageContract
- GreeterContract
-- Script
-- Ideally, you want to require() the contract rather than the module.
local Greeter = require(script.Parent.GreeterContract)
-- If you still return the module table in your module scripts, you can keep
-- require()ing the old module. If you ever face a cyclic module dependency,
-- require() the contract instead
local Message = require(script.Parent.MessageModule)
-- Important: The modules with the implementations will still need to be require()d!
-- If you make it a habit to only require() the contracts, this won't ever cause a
-- cyclic module dependency
require(script.Parent.GreeterModule)
Message.Say()
Greeter.Say("Hello world!")
Message.Say()
-- MessageModule
local ServerScriptService = game:GetService("ServerScriptService")
local Greeter = require(ServerScriptService.GreeterContract) -- Caution! We are now requiring the contract!
local Message = require(ServerScriptService.MessageContract) -- Caution! We are now requiring the contract!
local message = ""
function Message.SetMessage(msg: string)
message = msg
end
function Message.Say()
Greeter.Say(message)
end
return Message
-- GreeterModule
local ServerScriptService = game:GetService("ServerScriptService")
local Message = require(ServerScriptService.MessageContract) -- Caution! We are now requiring the contract!
local Greeter = require(ServerScriptService.GreeterContract) -- Caution! We are now requiring the contract!
function Greeter.Say(msg: string)
print(msg)
Message.SetMessage(msg .. " (1)")
end
-- You don't need to return the module table anymore.
-- It doesn't hurt to return it anyways though
return true
-- MessageContract
return {} :: {
-- Not necessary, but additional type safety is always nice
SetMessage: (message: string) -> (),
Say: () -> ()
}
-- GreeterContract
return {} :: {
-- Again, not necessary, but additional type safety is always nice
Say: (message: string) -> ()
}
Now this won’t cause any cyclic module dependency errors anymore, while still working as intended. Small gotcha: your functions may not be loaded in at the time of require()ing the module, so make sure to only start game logic after require()ing all the function implementation modules.
That’s all there is to it! So to recap:
- Create a contract module for each implementation module in your game.
- This contract module only returns a single table, nothing more.
- When defining new functions in a modulescript, do not create a new table for the functions. Use the contract table instead.
- Make sure to require() all the implementation modulepreferably from a single loader function or in a single script.
- Functions only exist after require()ing the relevant implementation module. If you require() all implementation modules, and only then start executing logic, this won’t ever be a problem.

