A lightweight, agnostic module loader and scheduler for Roblox. Eliminate load-order bugs and race conditions by organizing your modules into a deterministic pipeline. Define exactly what runs, and in what order, every single frame with zero ambiguity.
As Roblox projects scale, developers often fall into the trap of decentralized execution. Every module, controller or service connects to RunService independently, and, before long, you have dozens of scripts updating independently every frame. Without strict control over whether your InputController runs before or after your CharacterController, you inevitably run into state bugs, race conditions, and one-frame delays.
Bootstrapper solves this by treating execution flow as a single, managed pipeline. By organizing your modules into simple arrays and updating them in a strict sequence, you completely eliminate unpredictable load orders and execution desyncs. Bootstrapper makes sure things happen exactly when you tell them to and doesn't care if you write OOP, functional, or procedural code.
- Deterministic Execution: Load modules alphabetically or provide a strict manual sequence. Your game will start exactly the same way every single time.
- Centralized Scheduler: Iterate over your modules and fire their updates synchronously.
- Luau Method Syntax: Use standard dot or colon notation (
.or:) in your string arguments to explicitly call functions or injectselffor object methods. - Flexible Event Binding: Hook a sequence of modules directly to
RunServiceevents,RBXScriptSignals, or custom Signal objects. - Automatic Memory Profiling: Automatically assigns
debug.setmemorycategoryto every module's thread andRunServiceconnection so yourMicroProfileris readable. - Lazy Require: Pass raw
ModuleScriptinstances directly as argument. Bootstrapper will automatically require them.
Add this to your wally.toml:
ldgerrits/bootstrapper@^1.2.1
local Bootstrapper = require(packages.Bootstrapper)
-- Strict boot sequence (manual order for core systems)
local bootSequence = { path.to.DataService, path.to.PlayerService, path.to.ZoneService }
Bootstrapper.run(bootSequence, ':init') -- injects self
Bootstrapper.runAsync(bootSequence, ':start') -- injects self
-- Automatic discovery (A-Z sorted)
local systems = Bootstrapper.loadChildren(path.to.Systems, Bootstrapper.byName('System$'))
-- Use '.run' for a strict A-Z sequence, or '.runConcurrent()' if not
Bootstrapper.run(systems, '.init') -- does NOT inject self
Bootstrapper.runConcurrent(systems, '.start') -- does NOT inject self
-- Maintains alphabetical execution every frame with auto-memory profiling.
Bootstrapper.bindToHeartbeat(systems, '.onUpdate') -- does NOT inject self
-- Run every second without drift.
Bootstrapper.bindToInterval(systems, '.onTick', 1.0) -- does NOT inject selfLoad modules from a folder. Bootstrapper automatically requires them and returns a deterministically sorted array (alphabetical by Name), ensuring your game starts the same way every time.
local Bootstrapper = require(path.to.Bootstrapper)
local services, errors = Bootstrapper.loadDescendants(path.to.Services, Bootstrapper.byName('Service$'))You can use .loadSequence() to define a manual order using an array ModuleScript instances. This sequence can be stored and reused for all lifecycle stages.
local services, errors = Bootstrapper.loadSequence({
path.to.DataService,
path.to.PlayerService,
path.to.AssetService,
})
-- Use the resulting services array for everything else
Bootstrapper.run(services, '.init')When passing a method name to any Bootstrapper function, you can dictate how the function is executed:
-
'methodName'or':methodName': Calls as an object method.module:methodName() -
'.methodName': Calls as a function.module.methodName()
-- Injects 'self' into every module's '.onUpdate()' method
Bootstrapper.bindToHeartbeat(services, ':onUpdate') -- injects self
Bootstrapper.bindToHeartbeat(services, 'onUpdate') -- functionally the same as ':onUpdate'
-- Calls '.tick()' statically, without passing the module table
Bootstrapper.bindToInterval(services, '.tick', 1.0) -- does NOT inject selfYou don't have to call a loader function. Pass an array of ModuleScripts directly to any function, and Bootstrapper handles the require() automatically.
-- This works even if the modules haven't been required yet
Bootstrapper.bindToHeartbeat({
path.to.CombatService,
path.to.VFXService,
}, '.onUpdate')-
.run(): Sequential & Yielding. Yields until every module finishes in order. Returns successful modules and an error map.
-
.runAsync(): Sequential & Non-yielding. Executes the sequence in a background thread while maintaining order.
-
.runConcurrent(): Concurrent & Non-yielding. Fires all methods simultaneously. Best for independent tasks where order doesn't matter.
-- Sequential & Yielding
local services, errors = Bootstrapper.run(services, '.init')
-- Sequential & Non-yielding
Bootstrapper.runAsync(services, '.postInit')
-- Concurrent & Non-yielding
Bootstrapper.runConcurrent(services, '.start', gameState)Route RunService events or signals directly to module methods. All binding functions follow the same signature: (modules, methodName, [context]).
local cleanupRenderStep = Bootstrapper.bindToRenderStep(services, '.onRender', Enum.RenderPriority.Last.Value)
local cleanupInterval = Bootstrapper.bindToInterval(systems, '.onTick', 1.0)
local cleanupGameState = Bootstrapper.bindTo(services, '.onGameState', GameState.Changed)
-- Disconnect everything when they are no longer needed
cleanupRenderStep()
cleanupInterval()
cleanupGameState()