Solving cyclic module dependencies

.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.
9 Likes

This would indeed work for preventing a cyclic dependency, but it’s basically a bandaid for someone’s dirty code.

Almost all cases that I’ve seen of a cyclic dependency ( A ↔ B ) are scripts that violate the SRP (Single Responsibility Principle), of which one of the guilty scripts could be split up (A1 → B → A2). It could also just be as simple as changing the code slightly (in this case, have Greeter.Say() return the msg string, and then Message would call its own function with it), or a case of where it would make sense to combine the two scripts (AB).

Just a thought for everyone out there that rethinking your code structure can both fix your issues and make your code cleaner at the same time (otherwise, good tutorial on this issue!).

5 Likes

Hi, thanks for your comment!
Do you happen to have examples of the cyclic dependencies you’ve seen? I’d love to have some more material to reason about.

Personally I’ve grown this style of programming while creating a TDS game, where towers, enemies and the map interact with each other in complex manners, with interactions even being different between maps. This requires a lot of integration work, meaning that different systems inevitably need to interact with each other. Eliminating the entire class of dependency chain problems seems like the logical and cleaner solution, since it allows you to keep functions in a more logical place rather than needing to create a new module if it doesn’t fit nicely.

My main question is if my assumptions are wrong, and through which metrics (debugability, readability, maintainability, etc.) my system would be better off if I were / were not to follow the technique I’ve described.

2 Likes

A small example that I personally had one time was a building loader (BuildingLoader) module that handled loading a player’s owned building for my game. This module had a helper module (BuildingHelper) that required BuildingLoader and contained some helpful functions for modules trying to interact with it. However, the BuildingLoader also used some of BuildingHelper’s functions. Classic cyclic dependency.

After further analysis of my code, I realized that BuildingLoader had multiple responsibilities: loading the buildings and teleporting their owners to that building. BuildingHelper was also mainly used for helping with the teleporting of the building’s owners. The BuildingLoader could be cleanly separated to prevent this cyclic dependency. I also had the choice of potentially keeping BuildingLoader as-is and instead making the BuildingHelper only have functions that didn’t directly require the BuildingLoader. Then, I would put the functions that required BuildingLoader under the BuildingLoader module instead of BuildingHelper. However, I went with the first option as it was cleaner.


Another example that could be more specific to a TDS could be an interaction between a special enemy and a tower. The tower has a target ‘Enemy’ module that it’s damaging, and this special enemy that can stun towers has a target ‘Tower’ module. Yet another Cyclic dependency. In this case, lets assume the special enemy has a module called SpecialEnemy that handles all of the basic enemy logic + its special ability.

This would be yet another case of violating the Single Responsibility Principle. The better way to handle this case would be to have every enemy have the base ‘Enemy’ module. Then, special enemies would add on specific “ability” scripts to add their unique ability logic.


If you have any examples specific to your game, I’d be glad to try and help with eliminating that dependency chain.

4 Likes

Hi, thanks for the response! I’ve been a bit tired from work, so excuses for my late reply.

If I understand your first example correctly, you started off with:

  • BuildingLoader
    • Loads buildings
    • Teleports players to their building using BuildingHelper
    • Stores the location data of each player’s building
  • BuildingHelper
    • Utility functions to teleport players to buildings
    • Uses the BuildingLoader’s location data to teleport the player to their building

I’m not entirely sure what method you used to solve the cyclic dependency problem. Did you move the responsibility of teleporting players to BuildingHelper instead, or did to a completely new module altogether?
I’m also not certain what the logic around this is. Did BuildingLoader contain a function that first loads the player’s building, and teleport them immediately afterwards? Or do you have some business logic elsewhere that first loads the player’s building and then teleports them afterwards, or additionally whenever the player requests to teleport to their own base?

What I’d do is to remove the link between BuildingHelper and BuildingLoader through removing the responsibility from BuildingHelper to know where the building is located. A business logic layer could then retrieve the data from BuildingLoader and pass that into BuildingHelper so it knows where to teleport the player.
This does add the burden of needing a business logic layer to a helper that should ideally relieve you of then eed to query this location data anyways, so an even cleaner solution would be to simply stitch the location data into a globally accessible variable. You could create a Game modulescript which has the only responsibility of storing all game data, for example. The BuildingLoader could then put its data into this variable, and BuildingHelper could use this variable to know where to teleport the player to.
This has the additional benefit of easily allowing other systems to do things with the physical location of your building. I don’t know what game it was that you’ve developed, but maybe a police car could drive to your location to raid your base.


For the TDS game, let’s suppose we have a magical forest map, and the monsters walking around are magical beasts. If one of your towers throws a magic gemstone at a magical beast, it has a chance of eating the gemstone, greatly increasing its mana, and transforming into a giant magical beast. This giant magical beast will then kill off all other magical beasts it encounters, growing in strength with each magical beast. Once it’s strong enough, it creates a blast of mana, infusing several tiles with mana and killing all the towers that stand on those tiles. Towers placed on these mana-infused tiles will gain an attack boost. This blast might be strong enough to break a stone on a path, creating a shortcut for enemies on their way to your base.
This concept would require communication between:

  • The pathing system
    • Create a new path upon destroying a rock
  • The towers
    • Be killed upon explosion
    • Gain buff when standing on mana-infused tile
  • The enemies
    • Follow new path upon rock being destroyed
    • Eat other enemies upon becoming a giant
    • Know when they’re attacked by a tower with a gemstone
  • The map layout
    • Destroy rock model when needed
    • Mark relevant tiles as mana-infused

Additionally, I should be capable of thinking of more mechanics like this without needing to spend hours of time on rearchitecturing my game to fit the new mechanics. Each hour spent on rearchitecturing the game is, after all, an hour not spend on creating new content, which is what players will ultimately care about.
What architecture do you propose?

3 Likes

To solve the cyclic dependency problem, I separated the responsibilities of BuildingLoader: loading players’ buildings and also teleporting them to it 5 seconds afterwards (not immediately). These responsibilities were already two separate functions in the module, so separating them into [spoiler](these are example names)[/spoiler] BuildingLoader and PlayerTeleporter was easy, and removed the link between the two while also giving each module its own single responsibility.

(This is also the stripped-down version of my logic, I’d be glad to give you my full rundown of how I get all of my building-related logic working and clean without cyclic dependencies)

I really advise against having a main script holding all the data of your game. It’s good for ease of access and ease of changing a variable (in this case, maybe a dictionary of players → building), but this also means that ANY other module that uses this main script could see and change any variable they want, even if its not one they need or should ever interact with. Most of the time, you can just keep this globally-accessible variable inside the module that sets it (in this case, BuildingLoader) so that setting can remain private, while getting can still remain public to those that need it.


For your TDS game, you are more than right for wanting your game to be flexible enough to not require major re-architecting upon adding new mechanics. This is where having the least amount of coupling (no cyclic dependencies) and a high amount of cohesion (modules only having a single responsibility) comes in. I don’t understand your question about what architecture I would suppose (coding pattern?), but the only tip I have would be to look into the SOLID principles. This will allow you to make your game’s code as expandable and clean as possible.

My only situation when I needed cyclic module dependencies was status effects on objects. I have main controller which handles setting/removing effects, and it’s child which handles what each status effect does. And some effects like Burning need to spread - which required me to use parent module script. Solution I have found was requiring child module with self and storing it as variable ParentModule.