Skip to main content

Dependencies

Dependencies make Unit startup order explicit. A Unit declares the ids of other Units that must be available before it initializes or starts.

local Inventory = {}

Inventory.Id = "Inventory"
Inventory.Dependencies = { "Data" }

function Inventory:Init()
self.Data = self:Get("Data")
end

return Inventory

That single line says a lot: Inventory is not a standalone system. It needs the Data Unit. Rivet should boot Data first, and maintainers should read Data before they try to understand Inventory startup.

Dependency Shape

Dependencies is optional. When present, it must be an array of non-empty strings.

Unit.Dependencies = { "Data", "Economy", "Notifications" }

These are invalid:

Unit.Dependencies = "Data" -- not an array
Unit.Dependencies = { "" } -- empty id
Unit.Dependencies = { [2] = "Data" } -- array hole
Unit.Dependencies = { Data = true } -- dictionary, not array

Rivet normalizes missing dependencies to {}. That means a Unit without Dependencies is simply independent.

Boot Order

Rivet uses dependency sorting. Dependencies appear before dependents.

Given this graph:

Data

Inventory

Shop

And this code:

Data.Dependencies = {}
Inventory.Dependencies = { "Data" }
Shop.Dependencies = { "Inventory" }

Rivet runs lifecycle methods in this shape:

Data:Init()
Inventory:Init()
Shop:Init()
Data:Start()
Inventory:Start()
Shop:Start()

On destroy, the order reverses:

Shop:Destroy()
Shop.Clean:Cleanup()
Inventory:Destroy()
Inventory.Clean:Cleanup()
Data:Destroy()
Data.Clean:Cleanup()
Reverse destroy protects dependencies

If Inventory depends on Data, Inventory shuts down before Data. That gives Inventory:Destroy() a chance to use Data while cleaning up.

Init And Start With Dependencies

Init is the right place to cache dependency references.

function Inventory:Init()
self.Data = self:Get("Data")
end

Start is the right place to begin work that expects every Unit to be initialized.

function Inventory:Start()
for _, player in ipairs(Players:GetPlayers()) do
self:LoadPlayer(player)
end
end

The difference matters. If Inventory:Start() calls into Round, and Round does not depend on Inventory, Round has still completed Init because Rivet runs all Init methods before any Start.

Lookup Rules

Use self:Get(id) inside Units:

function Inventory:Init()
self.Data = self:Get("Data")
end

Use Rivet:Get(id) outside Units after startup:

local Inventory = Rivet:Get("Inventory")

Lookup returns the actual server Unit table on the server. On the client, lookup may return a client proxy for declared server surfaces.

Declare dependencies even if lookup would work

If Inventory:Start() calls self:Get("Data"), lookup may succeed as long as Data exists. But without Inventory.Dependencies = { "Data" }, startup order no longer documents the relationship. Declare the dependency so the graph is visible and destroy order is correct.

Missing Dependencies

If a Unit declares a dependency that was not loaded, Rivet fails startup:

Rivet missing dependency. Unit "Inventory" depends on missing unit "Data". Add a Unit with Id "Data" under config.Roots or remove it from Dependencies.

Check these things:

  • Does a Unit with Id = "Data" exist?
  • Is the Unit under one of the roots passed to Rivet.Start?
  • Did the Unit omit Id, meaning its ModuleScript name must be "Data"?
  • Did you typo the dependency string?
  • Did startup fail earlier while requiring the dependency Unit?

Circular Dependencies

Rivet rejects cycles.

A.Dependencies = { "B" }
B.Dependencies = { "C" }
C.Dependencies = { "A" }

Startup error:

Rivet circular dependency. Circular Rivet dependency detected: A -> B -> C -> A

Cycles usually mean responsibilities are tangled. Do not solve them by hiding a lookup in Start or moving work to top-level requires. Instead, split one system, introduce a mediator Unit, or invert the relationship through a Signal or callback.

Dependency Design Patterns

Direct Ownership

Use direct dependencies when one Unit clearly owns data another Unit needs.

Inventory.Dependencies = { "Data" }
Shop.Dependencies = { "Data", "Inventory" }

Data owns profiles. Inventory reads items. Shop charges currency and adds items. The graph tells the story.

Event Fanout

If many Units need to react to one event, a direct dependency graph can get noisy. A Notifications or Events Unit can coordinate shared event streams, or the owner Unit can expose Signals to clients while server-side Units call methods directly.

Round.Dependencies = { "Notifications" }
Quests.Dependencies = { "Notifications" }

Read-Only Helpers

Do not turn static helpers into Units just so they can be dependencies. If something is a pure function or static table, require it normally.

local ItemDefinitions = require(ReplicatedStorage.Shared.ItemDefinitions)

Dependencies are for managed Units, not every module import.

Checklist

  • Add Dependencies when one Unit requires another Unit's initialized state.
  • Store dependency references in Init.
  • Keep expensive work in Start.
  • Avoid circular dependencies by splitting responsibilities.
  • Do not use dependency arrays for helper modules.
  • Use dependency ids, not file paths.

Next: Lifecycle And Cleanup.