The Service Registry Design Pattern in Roblox Luau: An Advanced Implementation Guide
Introduction to the Service Registry Pattern with Static Typing
When developing complex Roblox experiences, organizing your code effectively becomes essential for maintaining scalability, testability, and collaboration. The Service Registry pattern provides a structured approach to managing various systems that power your game. In this comprehensive guide, we’ll explore advanced implementations using Luau’s powerful static typing system, focusing exclusively on solutions that leverage the Roblox API without external dependencies.
Advanced Registry Implementation
Let’s implement a fully-featured Service Registry with Luau’s static typing:
--!strict
-- ServiceRegistry.lua
local RunService = game:GetService("RunService")
-- Import type definitions
-- ServiceRegistry implementation
local ServiceRegistry = {} :: ServiceRegistry
ServiceRegistry.__index = ServiceRegistry
-- Private properties (using closures for true privacy)
local function createPrivateState()
return {
services = {} :: {[string]: ServiceInterface},
metadata = {} :: {[string]: ServiceMetadata},
events = {
serviceRegistered = createEvent(),
serviceUnregistered = createEvent(),
serviceInitializing = createEvent(),
serviceInitialized = createEvent(),
serviceStarting = createEvent(),
serviceStarted = createEvent(),
serviceStopping = createEvent(),
serviceStopped = createEvent(),
serviceFailed = createEvent()
},
debugMode = false
}
end
-- Constructor
function ServiceRegistry.new(): ServiceRegistry
local self = setmetatable({}, ServiceRegistry)
local private = createPrivateState()
-- Create reference to private state through closure
-- This simulates truly private fields while maintaining type safety
self._getPrivate = function()
return private
end
return self
end
local function updateServiceStatus(self: ServiceRegistry, serviceName: string, status: string, errorMessage: string?)
local private = self._getPrivate()
if not private.metadata[serviceName] then
return
end
private.metadata[serviceName].status = status
if errorMessage then
private.metadata[serviceName].lastError = errorMessage
end
-- Fire appropriate event based on service status
end
-- Public API implementation
function ServiceRegistry:RegisterService(name: string, service: ServiceInterface, options: ServiceInitOptions?): boolean
local private = self._getPrivate()
if private.services[name] then
warn(string.format("Service '%s' is already registered.", name))
return false
end
-- Default options
options = options or {}
options.dependencies = options.dependencies or {}
options.autoStart = (options.autoStart ~= nil) and options.autoStart or false
options.priority = options.priority or 0
options.tags = options.tags or {}
-- Register the service
private.services[name] = service
-- Create metadata
private.metadata[name] = {
name = name,
status = STATUS.UNINITIALIZED,
dependencies = options.dependencies,
dependents = {},
options = options,
registrationTime = os.clock(),
initializationTime = nil,
startTime = nil,
lastError = nil
}
-- Update dependent metadata for each dependency
for _, dep in ipairs(options.dependencies) do
if private.metadata[dep.name] then
table.insert(private.metadata[dep.name].dependents, name)
else
warn(string.format("Service '%s' depends on unregistered service '%s'", name, dep.name))
end
end
debugLog(self, string.format("Registered service '%s'", name))
private.events.serviceRegistered:_fire(name)
-- Check for circular dependencies
local hasCycle, cyclePath = self:CheckCircularDependencies()
if hasCycle then
warn(string.format("Circular dependency detected: %s", table.concat(cyclePath or {}, " -> ")))
end
return true
end
function ServiceRegistry:UnregisterService(name: string): boolean
local private = self._getPrivate()
if not private.services[name] then
return false
end
-- Check if service has dependents
if #private.metadata[name].dependents > 0 then
warn(string.format("Cannot unregister service '%s' while it has dependents: %s",
name, table.concat(private.metadata[name].dependents, ", ")))
return false
end
-- Stop the service if it's running
if private.metadata[name].status == STATUS.STARTED then
self:StopService(name)
end
-- Remove the service
private.services[name] = nil
private.metadata[name] = nil
debugLog(self, string.format("Unregistered service '%s'", name))
private.events.serviceUnregistered:_fire(name)
return true
end
function ServiceRegistry:GetService(name: string): ServiceInterface
local private = self._getPrivate()
if not private.services[name] then
error(string.format("Service '%s' not found", name), 2)
end
return private.services[name]
end
function ServiceRegistry:GetServiceAsync(name: string): ServiceInterface
local private = self._getPrivate()
-- If the service exists, return it immediately
if private.services[name] then
return private.services[name]
end
-- Wait for the service to be registered (with a timeout)
local startTime = os.clock()
local maxWaitTime = 10 -- seconds
while os.clock() - startTime < maxWaitTime do
if private.services[name] then
return private.services[name]
end
task.wait(0.1)
end
error(string.format("Timeout waiting for service '%s'", name), 2)
end
function ServiceRegistry:HasService(name: string): boolean
local private = self._getPrivate()
return private.services[name] ~= nil
end
function ServiceRegistry:GetAllServices(): {[string]: ServiceInterface}
local private = self._getPrivate()
return table.clone(private.services)
end
function ServiceRegistry:GetServicesByTag(tag: string): {ServiceInterface}
local private = self._getPrivate()
local result = {}
for name, metadata in pairs(private.metadata) do
if table.find(metadata.options.tags, tag) then
table.insert(result, private.services[name])
end
end
return result
end
function ServiceRegistry:InitializeService(name: string): boolean
local private = self._getPrivate()
if not private.services[name] then
error(string.format("Service '%s' not found", name), 2)
end
-- Already initialized
if private.metadata[name].status == STATUS.INITIALIZED or
private.metadata[name].status == STATUS.STARTING or
private.metadata[name].status == STATUS.STARTED then
return true
end
-- Already initializing
if private.metadata[name].status == STATUS.INITIALIZING then
warn(string.format("Service '%s' is already being initialized", name))
return false
end
debugLog(self, string.format("Initializing service '%s'", name))
updateServiceStatus(self, name, STATUS.INITIALIZING)
-- Initialize dependencies first
for _, dependency in ipairs(private.metadata[name].dependencies) do
if not private.services[dependency.name] then
if dependency.required then
updateServiceStatus(self, name, STATUS.FAILED,
string.format("Required dependency '%s' not found", dependency.name))
return false
else
warn(string.format("Optional dependency '%s' not found for service '%s'",
dependency.name, name))
continue
end
end
-- Skip initializing this dependency if it's marked as initialization-only and already started
if dependency.initializationOnly and
(private.metadata[dependency.name].status == STATUS.STARTED) then
continue
end
-- Recursively initialize the dependency
if not self:InitializeService(dependency.name) and dependency.required then
updateServiceStatus(self, name, STATUS.FAILED,
string.format("Failed to initialize required dependency '%s'", dependency.name))
return false
end
end
-- Initialize the service
local service = private.services[name]
local success, errorMessage = pcall(function()
return service:Initialize(self)
end)
if not success or errorMessage == false then
if type(errorMessage) ~= "string" then
errorMessage = "Service returned false from Initialize"
end
updateServiceStatus(self, name, STATUS.FAILED, errorMessage)
return false
end
-- Mark as initialized
private.metadata[name].initializationTime = os.clock()
updateServiceStatus(self, name, STATUS.INITIALIZED)
-- Auto-start if configured
if private.metadata[name].options.autoStart then
task.defer(function()
self:StartService(name)
end)
end
return true
end
function ServiceRegistry:InitializeAllServices(): boolean
local private = self._getPrivate()
local allSuccess = true
-- Sort services by priority
local serviceNames = {}
for name, metadata in pairs(private.metadata) do
table.insert(serviceNames, {
name = name,
priority = metadata.options.priority
})
end
table.sort(serviceNames, function(a, b)
return a.priority > b.priority
end)
-- Initialize in priority order
for _, service in ipairs(serviceNames) do
if not self:InitializeService(service.name) then
allSuccess = false
end
end
return allSuccess
end
function ServiceRegistry:StartService(name: string): boolean
local private = self._getPrivate()
if not private.services[name] then
error(string.format("Service '%s' not found", name), 2)
end
-- Already started
if private.metadata[name].status == STATUS.STARTED then
return true
end
-- Already starting
if private.metadata[name].status == STATUS.STARTING then
warn(string.format("Service '%s' is already being started", name))
return false
end
-- Not initialized yet
if private.metadata[name].status ~= STATUS.INITIALIZED then
if not self:InitializeService(name) then
return false
end
end
debugLog(self, string.format("Starting service '%s'", name))
updateServiceStatus(self, name, STATUS.STARTING)
-- Start dependencies first (except initialization-only ones)
for _, dependency in ipairs(private.metadata[name].dependencies) do
if dependency.initializationOnly then
continue
end
if not self:StartService(dependency.name) and dependency.required then
updateServiceStatus(self, name, STATUS.FAILED,
string.format("Failed to start required dependency '%s'", dependency.name))
return false
end
end
-- Start the service
local service = private.services[name]
local success, errorMessage = pcall(function()
return service:Start(self)
end)
if not success or errorMessage == false then
if type(errorMessage) ~= "string" then
errorMessage = "Service returned false from Start"
end
updateServiceStatus(self, name, STATUS.FAILED, errorMessage)
return false
end
-- Mark as started
private.metadata[name].startTime = os.clock()
updateServiceStatus(self, name, STATUS.STARTED)
return true
end
function ServiceRegistry:StartAllServices(): boolean
local private = self._getPrivate()
local allSuccess = true
-- Sort services by priority
local serviceNames = {}
for name, metadata in pairs(private.metadata) do
table.insert(serviceNames, {
name = name,
priority = metadata.options.priority
})
end
table.sort(serviceNames, function(a, b)
return a.priority > b.priority
end)
-- Start in priority order
for _, service in ipairs(serviceNames) do
if not self:StartService(service.name) then
allSuccess = false
end
end
return allSuccess
end
function ServiceRegistry:StopService(name: string): boolean
local private = self._getPrivate()
if not private.services[name] then
error(string.format("Service '%s' not found", name), 2)
end
-- Not started
if private.metadata[name].status ~= STATUS.STARTED then
return true
end
-- Check for dependents that are still running
local runningDependents = {}
for _, dependentName in ipairs(private.metadata[name].dependents) do
if private.metadata[dependentName].status == STATUS.STARTED and
not table.find(private.metadata[dependentName].dependencies, function(dep)
return dep.name == name and dep.initializationOnly
end) then
table.insert(runningDependents, dependentName)
end
end
if #runningDependents > 0 then
warn(string.format("Cannot stop service '%s' while dependents are running: %s",
name, table.concat(runningDependents, ", ")))
return false
end
debugLog(self, string.format("Stopping service '%s'", name))
updateServiceStatus(self, name, STATUS.STOPPING)
-- Stop the service
local service = private.services[name]
local success, errorMessage = pcall(function()
return service:Stop(self)
end)
if not success or errorMessage == false then
if type(errorMessage) ~= "string" then
errorMessage = "Service returned false from Stop"
end
updateServiceStatus(self, name, STATUS.FAILED, errorMessage)
return false
end
-- Mark as stopped
updateServiceStatus(self, name, STATUS.STOPPED)
return true
end
function ServiceRegistry:StopAllServices(): boolean
local private = self._getPrivate()
local allSuccess = true
-- Sort services by inverse priority (stop highest priority last)
local serviceNames = {}
for name, metadata in pairs(private.metadata) do
if metadata.status == STATUS.STARTED then
table.insert(serviceNames, {
name = name,
priority = metadata.options.priority
})
end
end
table.sort(serviceNames, function(a, b)
return a.priority < b.priority
end)
-- Stop in inverse priority order
for _, service in ipairs(serviceNames) do
if not self:StopService(service.name) then
allSuccess = false
end
end
return allSuccess
end
function ServiceRegistry:GetServiceDependencies(name: string): {ServiceDependency}
local private = self._getPrivate()
if not private.metadata[name] then
error(string.format("Service '%s' not found", name), 2)
end
return table.clone(private.metadata[name].dependencies)
end
function ServiceRegistry:GetServiceDependents(name: string): {string}
local private = self._getPrivate()
if not private.metadata[name] then
error(string.format("Service '%s' not found", name), 2)
end
return table.clone(private.metadata[name].dependents)
end
function ServiceRegistry:CheckCircularDependencies(): (boolean, {string}?)
local private = self._getPrivate()
for name, _ in pairs(private.services) do
local hasCycle, cyclePath = findCircularDependencies(self, name)
if hasCycle then
return true, cyclePath
end
end
return false, nil
end
function ServiceRegistry:GetServiceStatus(name: string): string
local private = self._getPrivate()
if not private.metadata[name] then
error(string.format("Service '%s' not found", name), 2)
end
return private.metadata[name].status
end
function ServiceRegistry:EnableDebugMode(enabled: boolean): ()
local private = self._getPrivate()
private.debugMode = enabled
end
function ServiceRegistry:GetDiagnosticInfo(): {[string]: any}
local private = self._getPrivate()
local result = {
services = {},
stats = {
totalServices = 0,
initialized = 0,
started = 0,
failed = 0
}
}
for name, metadata in pairs(private.metadata) do
result.stats.totalServices += 1
if metadata.status == STATUS.INITIALIZED or metadata.status == STATUS.STARTED then
result.stats.initialized += 1
end
if metadata.status == STATUS.STARTED then
result.stats.started += 1
end
if metadata.status == STATUS.FAILED then
result.stats.failed += 1
end
-- Clone the metadata to avoid exposing internal references
result.services[name] = table.clone(metadata)
end
return result
end
-- Export the constants
ServiceRegistry.STATUS = STATUS
return ServiceRegistry
Creating Well-Structured Services
Base Service Template
Here’s a template for creating statically-typed services:
--!strict
-- BaseService.lua
local Types = require(script.Parent.Parent.Types)
type ServiceInterface = Types.ServiceInterface
type ServiceRegistry = Types.ServiceRegistry
-- Base service template with default implementations
local BaseService = {}
BaseService.__index = BaseService
function BaseService.new(): ServiceInterface
local self = setmetatable({}, BaseService)
-- Common service properties
self._initialized = false
self._started = false
self._registry = nil
return self
end
function BaseService:Initialize(registry: ServiceRegistry): boolean
if self._initialized then
return true
end
self._registry = registry
self._initialized = true
return true
end
function BaseService:Start(registry: ServiceRegistry): boolean
if not self._initialized then
error("Service must be initialized before starting", 2)
end
if self._started then
return true
end
self._started = true
return true
end
function BaseService:Stop(registry: ServiceRegistry): boolean
if not self._started then
return true
end
self._started = false
return true
end
function BaseService:GetStatus(): string
if not self._initialized then
return "UNINITIALIZED"
elseif not self._started then
return "INITIALIZED"
else
return "STARTED"
end
end
return BaseService
Concrete Service Implementation
Let’s implement a data service with static typing:
--!strict
-- DataService.lua
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local Types = require(script.Parent.Parent.Types)
type ServiceInterface = Types.ServiceInterface
type ServiceRegistry = Types.ServiceRegistry
-- Define data structures with type safety
type PlayerData = {
coins: number,
experience: number,
level: number,
items: {[string]: ItemData},
lastLogin: number,
totalPlayTime: number
}
type ItemData = {
id: string,
quantity: number,
metadata: {[string]: any}?
}
type SaveRequest = {
player: Player,
userId: number,
data: PlayerData,
timestamp: number
}
-- Default data template
local DEFAULT_PLAYER_DATA: PlayerData = {
coins = 100,
experience = 0,
level = 1,
items = {},
lastLogin = 0,
totalPlayTime = 0
}
-- DataService implementation
local DataService = {}
DataService.__index = DataService
function DataService.new(): ServiceInterface
local self = setmetatable({}, DataService)
-- Private properties
self._initialized = false
self._started = false
self._registry = nil
self._dataStore = nil
self._cache = {} :: {[number]: PlayerData}
self._saveQueue = {} :: {SaveRequest}
self._connections = {} :: {[string]: RBXScriptConnection}
self._isSaving = false
self._savingTask = nil
return self
end
function DataService:Initialize(registry: ServiceRegistry): boolean
if self._initialized then
return true
end
self._registry = registry
-- Initialize DataStore
local success, result = pcall(function()
return DataStoreService:GetDataStore("PlayerData")
end)
if not success then
warn("Failed to initialize DataStore: " .. tostring(result))
return false
end
self._dataStore = result
self._initialized = true
return true
end
function DataService:Start(registry: ServiceRegistry): boolean
if not self._initialized then
error("DataService must be initialized before starting", 2)
end
if self._started then
return true
end
-- Connect player events
self._connections.playerAdded = Players.PlayerAdded:Connect(function(player)
self:_handlePlayerJoin(player)
end)
self._connections.playerRemoving = Players.PlayerRemoving:Connect(function(player)
self:_handlePlayerLeave(player)
end)
-- Handle existing players
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(function()
self:_handlePlayerJoin(player)
end)
end
-- Start save queue processor
self._savingTask = task.spawn(function()
self:_processSaveQueue()
end)
-- Set up auto-save interval
self._connections.autoSave = RunService.Heartbeat:Connect(function()
if os.clock() % 300 < 1 then -- Every 5 minutes (approximately)
self:SaveAllData()
end
end)
self._started = true
return true
end
function DataService:Stop(registry: ServiceRegistry): boolean
if not self._started then
return true
end
-- Disconnect all events
for _, connection in pairs(self._connections) do
connection:Disconnect()
end
self._connections = {}
-- Save all remaining data
self:SaveAllData(true) -- Force immediate save
-- Clean up save queue processor
if self._savingTask then
task.cancel(self._savingTask)
self._savingTask = nil
end
self._started = false
return true
end
-- Private methods
function DataService:_handlePlayerJoin(player: Player)
local userId = player.UserId
if userId <= 0 then
return -- Skip non-persistent players (e.g. Studio)
end
-- Load data from DataStore
local data = self:LoadData(player)
-- Update last login time
data.lastLogin = os.time()
-- Store in cache
self._cache[userId] = data
end
function DataService:_handlePlayerLeave(player: Player)
local userId = player.UserId
if userId <= 0 then
return
end
-- Save player data
self:SaveData(player)
-- Remove from cache after a short delay to handle rejoins
task.delay(30, function()
self._cache[userId] = nil
end)
end
function DataService:_processSaveQueue()
--auto save
end
-- Public API
function DataService:LoadData(player: Player): PlayerData
if not self._initialized then
error("DataService is not initialized", 2)
end
local userId = player.UserId
-- Return cached data if available
if self._cache[userId] then
return table.clone(self._cache[userId])
end
-- Default data structure
local data = table.clone(DEFAULT_PLAYER_DATA)
-- Try to load from DataStore
local success, result = pcall(function()
return self._dataStore:GetAsync(tostring(userId))
end)
if success and result then
-- Merge with default data to ensure all fields exist
for key, value in pairs(result) do
-- Type checking for key fields
end
end
-- Store in cache
self._cache[userId] = data
return table.clone(data)
end
function DataService:SaveData(player: Player, immediate: boolean?): boolean
if not self._initialized then
error("DataService is not initialized", 2)
end
local userId = player.UserId
if userId <= 0 then
return false
end
-- Must have data to save
if not self._cache[userId] then
return false
end
-- Create save request
local request: SaveRequest = {
player = player,
userId = userId,
data = table.clone(self._cache[userId]),
timestamp = os.time()
}
-- Save immediately (blocking)
return true
else
-- Add to save queue
table.insert(self._saveQueue, request)
return true
end
end
function DataService:SaveAllData(immediate: boolean?): boolean
local allSuccess = true
for _, player in ipairs(Players:GetPlayers()) do
local success = self:SaveData(player, immediate)
if not success then
allSuccess = false
end
end
return allSuccess
end
function DataService:UpdateData(player: Player, updateFunction: (data: PlayerData) -> ())
if not self._initialized then
error("DataService is not initialized", 2)
end
-- Queue for saving
self:SaveData(player)
end
function DataService:GetStatus(): string
if not self._initialized then
return "UNINITIALIZED"
elseif not self._started then
return "INITIALIZED"
else
return "STARTED"
end
end
return DataService
Implementing Advanced Service Communication
Typed Event Service
--!strict
-- EventService.lua
local Types = require(script.Parent.Parent.Types)
type ServiceInterface = Types.ServiceInterface
type ServiceRegistry = Types.ServiceRegistry
-- Type definitions for the event system
export type EventCallback = (...any) -> ()
export type EventConnection = {
Disconnect: (self: EventConnection) -> (),
IsConnected: (self: EventConnection) -> boolean
}
export type Event = {
Connect: (self: Event, callback: EventCallback) -> EventConnection,
Once: (self: Event, callback: EventCallback) -> EventConnection,
Wait: (self: Event) -> ...any,
Fire: (self: Event, ...any) -> ()
}
-- EventService implementation
local EventService = {}
EventService.__index = EventService
function EventService.new(): ServiceInterface
local self = setmetatable({}, EventService)
-- Private properties
self._initialized = false
self._started = false
self._registry = nil
self._events = {} :: {[string]: Event}
return self
end
-- Create a new event object
local function createEvent(): Event
local connections = {}
local waitingThreads = {}
local firing = false
local queuedArgs = nil
-- Create connection object
local function createConnection(callback: EventCallback, once: boolean): EventConnection
local connection = {
_connected = true,
_callback = callback,
_once = once
}
function connection:Disconnect()
self._connected = false
-- Remove from connections table
for i, conn in ipairs(connections) do
if conn == self then
table.remove(connections, i)
break
end
end
end
function connection:IsConnected(): boolean
return self._connected
end
return connection
end
-- Create event object
local event = {}
function event:Connect(callback: EventCallback): EventConnection
assert(type(callback) == "function", "Callback must be a function")
local connection = createConnection(callback, false)
table.insert(connections, connection)
-- If there are queued args and we're not firing, fire this connection immediately
if queuedArgs and not firing then
task.spawn(function()
if connection._connected then
callback(unpack(queuedArgs))
end
end)
end
return connection
end
function event:Once(callback: EventCallback): EventConnection
assert(type(callback) == "function", "Callback must be a function")
local connection = createConnection(callback, true)
table.insert(connections, connection)
-- If there are queued args and we're not firing, fire this connection immediately
if queuedArgs and not firing then
task.spawn(function()
if connection._connected then
callback(unpack(queuedArgs))
connection:Disconnect()
end
end)
end
return connection
end
function event:Wait(): ...any
local thread = coroutine.running()
table.insert(waitingThreads, thread)
-- If there are queued args, resume this thread immediately
if queuedArgs then
table.remove(waitingThreads, table.find(waitingThreads, thread))
return unpack(queuedArgs)
end
return coroutine.yield()
end
function event:Fire(...: any)
local args = {...}
queuedArgs = args
firing = true
-- Resume all waiting threads
for _, thread in ipairs(waitingThreads) do
task.spawn(function()
coroutine.resume(thread, unpack(args))
end)
end
waitingThreads = {}
-- Fire all connections
local i = 1
while i <= #connections do
local connection = connections[i]
if connection._connected then
task.spawn(function()
connection._callback(unpack(args))
end)
if connection._once then
connection:Disconnect()
-- Don't increment i, since the table has shifted
else
i += 1
end
else
table.remove(connections, i)
-- Don't increment i, since the table has shifted
end
end
firing = false
end
return event
end
-- Service implementation
function EventService:Initialize(registry: ServiceRegistry): boolean
if self._initialized then
return true
end
self._registry = registry
self._initialized = true
return true
end
function EventService:Start(registry: ServiceRegistry): boolean
if not self._initialized then
error("EventService must be initialized before starting", 2)
end
if self._started then
return true
end
self._started = true
return true
end
function EventService:Stop(registry: ServiceRegistry): boolean
if not self._started then
return true
end
-- Clear all events
self._events = {}
self._started = false
return true
end
-- Public API
function EventService:GetEvent(eventName: string): Event
if not self._events[eventName] then
self._events[eventName] = createEvent()
end
return self._events[eventName]
end
function EventService:FireEvent(eventName: string, ...: any)
local event = self:GetEvent(eventName)
event:Fire(...)
end
function EventService:GetStatus(): string
if not self._initialized then
return "UNINITIALIZED"
elseif not self._started then
return "INITIALIZED"
else
return "STARTED"
end
end
return EventService
Server-Client Communication Service
--!strict
-- NetworkService.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Types = require(script.Parent.Parent.Types)
type ServiceInterface = Types.ServiceInterface
type ServiceRegistry = Types.ServiceRegistry
-- Type definitions for the network system
export type RemoteHandlerCallback = (player: Player, ...any) -> ...any
export type RemoteHandler = {
name: string,
callback: RemoteHandlerCallback,
validateArgs: ((...any) -> boolean)?
}
-- NetworkService implementation
local NetworkService = {}
NetworkService.__index = NetworkService
function NetworkService.new(): ServiceInterface
local self = setmetatable({}, NetworkService)
-- Private properties
self._initialized = false
self._started = false
self._registry = nil
self._isServer = RunService:IsServer()
self._remotes = {}
self._eventHandlers = {} :: {[string]: RemoteHandler}
self._functionHandlers = {} :: {[string]: RemoteHandler}
return self
end
function NetworkService:Initialize(registry: ServiceRegistry): boolean
if self._initialized then
return true
end
self._registry = registry
if self._isServer then
-- Server-side: Create remote instances
else
-- Client-side: Find remote instances
end
-- Set up event handlers
if self._isServer then
end
self._initialized = true
return true
end
function NetworkService:Start(registry: ServiceRegistry): boolean
if not self._initialized then
error("NetworkService must be initialized before starting", 2)
end
if self._started then
return true
end
self._started = true
return true
end
function NetworkService:Stop(registry: ServiceRegistry): boolean
if not self._started then
return true
end
-- Clear all handlers
self._eventHandlers = {}
self._functionHandlers = {}
-- Clean up remotes if server
if self._isServer and self._remotes.folder then
self._remotes.folder:Destroy()
end
self._started = false
return true
end
-- Private methods
function NetworkService:_handleRemoteEvent(player: Player?, handlerName: string, ...: any)
local handler = self._eventHandlers[handlerName]
if not handler then
warn(string.format("No handler registered for remote event: %s", handlerName))
return
end
if handler.validateArgs and not handler.validateArgs(...) then
warn(string.format("Invalid arguments for remote event: %s", handlerName))
return
end
-- Execute the handler
handler.callback(player, ...)
end
function NetworkService:_handleRemoteFunction(player: Player?, handlerName: string, ...: any): ...any
local handler = self._functionHandlers[handlerName]
if not handler then
warn(string.format("No handler registered for remote function: %s", handlerName))
return nil
end
if handler.validateArgs and not handler.validateArgs(...) then
warn(string.format("Invalid arguments for remote function: %s", handlerName))
return nil
end
-- Execute the handler and return results
return handler.callback(player, ...)
end
-- Public API
function NetworkService:RegisterEventHandler(
handlerName: string,
callback: RemoteHandlerCallback,
validateArgs: ((...any) -> boolean)?
)
if self._eventHandlers[handlerName] then
warn(string.format("Overwriting existing event handler: %s", handlerName))
end
self._eventHandlers[handlerName] = {
name = handlerName,
callback = callback,
validateArgs = validateArgs
}
end
function NetworkService:RegisterFunctionHandler(
handlerName: string,
callback: RemoteHandlerCallback,
validateArgs: ((...any) -> boolean)?
)
if self._functionHandlers[handlerName] then
warn(string.format("Overwriting existing function handler: %s", handlerName))
end
self._functionHandlers[handlerName] = {
name = handlerName,
callback = callback,
validateArgs = validateArgs
}
end
function NetworkService:UnregisterEventHandler(handlerName: string)
self._eventHandlers[handlerName] = nil
end
function NetworkService:UnregisterFunctionHandler(handlerName: string)
self._functionHandlers[handlerName] = nil
end
-- Client-to-server and server-to-client communication
if RunService:IsServer() then
-- Server-side methods
function NetworkService:FireClient(player: Player, handlerName: string, ...: any)
self._remotes.eventRemote:FireClient(player, handlerName, ...)
end
function NetworkService:FireAllClients(handlerName: string, ...: any)
self._remotes.eventRemote:FireAllClients(handlerName, ...)
end
function NetworkService:FireAllClientsExcept(excludedPlayer: Player, handlerName: string, ...: any)
for _, player in ipairs(game.Players:GetPlayers()) do
if player ~= excludedPlayer then
self._remotes.eventRemote:FireClient(player, handlerName, ...)
end
end
end
function NetworkService:InvokeClient(player: Player, handlerName: string, ...: any): ...any
local success, result = pcall(function()
return self._remotes.functionRemote:InvokeClient(player, handlerName, ...)
end)
if not success then
warn(string.format("Failed to invoke client %s: %s", handlerName, tostring(result)))
return nil
end
return result
end
else
-- Client-side methods
function NetworkService:FireServer(handlerName: string, ...: any)
self._remotes.eventRemote:FireServer(handlerName, ...)
end
function NetworkService:InvokeServer(handlerName: string, ...: any): ...any
local success, result = pcall(function()
return self._remotes.functionRemote:InvokeServer(handlerName, ...)
end)
if not success then
warn(string.format("Failed to invoke server %s: %s", handlerName, tostring(result)))
return nil
end
return result
end
end
function NetworkService:GetStatus(): string
if not self._initialized then
return "UNINITIALIZED"
elseif not self._started then
return "INITIALIZED"
else
return "STARTED"
end
end
return NetworkService
Real-World Application: Game State Management
Let’s implement a comprehensive game state management system:
--!strict
-- GameStateService.lua
local Players = game:GetService("Players")
local Types = require(script.Parent.Parent.Types)
type ServiceInterface = Types.ServiceInterface
type ServiceRegistry = Types.ServiceRegistry
-- Type definitions for the game state system
export type GameStateDefinition = {
name: string,
canEnterFrom: {string}?,
canExitTo: {string}?,
allowPlayerJoin: boolean?,
onEnter: ((gameState: GameStateService, previousState: string?, data: any?) -> ())?
onExit: ((gameState: GameStateService, nextState: string?, data: any?) -> ())?
update: ((gameState: GameStateService, deltaTime: number) -> ())?
}
export type GameStateTransitionEvent = (
stateName: string,
previousState: string?,
data: any?
) -> ()
export type GameStateMetadata = {
name: string,
enterTime: number,
data: any?
}
-- GameStateService implementation
local GameStateService = {}
GameStateService.__index = GameStateService
-- Predefined game states
GameStateService.States = {
NONE = "None",
LOBBY = "Lobby",
INTERMISSION = "Intermission",
LOADING = "Loading",
RUNNING = "Running",
ROUND_ENDING = "RoundEnding",
GAME_OVER = "GameOver"
}
function GameStateService.new(): ServiceInterface
local self = setmetatable({}, GameStateService)
-- Private properties
self._initialized = false
self._started = false
self._registry = nil
self._stateDefinitions = {} :: {[string]: GameStateDefinition}
self._currentState = nil :: string?
self._previousState = nil :: string?
self._stateMetadata = {} :: {[string]: GameStateMetadata}
self._listeners = {} :: {GameStateTransitionEvent}
self._updateConnection = nil :: RBXScriptConnection?
return self
end
function GameStateService:Initialize(registry: ServiceRegistry): boolean
if self._initialized then
return true
end
self._registry = registry
-- Register default states
self:RegisterState({
name = self.States.LOBBY,
allowPlayerJoin = true,
onEnter = function(gameState, previousState, data)
-- Default lobby behavior
local networkService = self._registry:GetService("NetworkService")
networkService:FireAllClients("GameStateChanged", self.States.LOBBY, data)
end
})
self:RegisterState({
name = self.States.INTERMISSION,
canEnterFrom = {self.States.GAME_OVER, self.States.LOBBY},
canExitTo = {self.States.LOADING},
allowPlayerJoin = true,
onEnter = function(gameState, previousState, data)
-- Default intermission behavior
local networkService = self._registry:GetService("NetworkService")
networkService:FireAllClients("GameStateChanged", self.States.INTERMISSION, data)
-- Schedule transition to next state after delay
task.delay(data and data.duration or 10, function()
if self._currentState == self.States.INTERMISSION then
self:TransitionToState(self.States.LOADING)
end
end)
end
})
self:RegisterState({
name = self.States.LOADING,
canEnterFrom = {self.States.INTERMISSION},
canExitTo = {self.States.RUNNING},
allowPlayerJoin = false,
onEnter = function(gameState, previousState, data)
-- Default loading behavior
local networkService = self._registry:GetService("NetworkService")
networkService:FireAllClients("GameStateChanged", self.States.LOADING, data)
-- Simulate loading time
task.delay(data and data.duration or 3, function()
if self._currentState == self.States.LOADING then
self:TransitionToState(self.States.RUNNING)
end
end)
end
})
self:RegisterState({
name = self.States.RUNNING,
canEnterFrom = {self.States.LOADING},
canExitTo = {self.States.ROUND_ENDING},
allowPlayerJoin = false,
onEnter = function(gameState, previousState, data)
-- Default game running behavior
local networkService = self._registry:GetService("NetworkService")
networkService:FireAllClients("GameStateChanged", self.States.RUNNING, data)
end,
update = function(gameState, deltaTime)
-- Game logic update code here
end
})
self:RegisterState({
name = self.States.ROUND_ENDING,
canEnterFrom = {self.States.RUNNING},
canExitTo = {self.States.GAME_OVER},
allowPlayerJoin = false,
onEnter = function(gameState, previousState, data)
-- Default round ending behavior
local networkService = self._registry:GetService("NetworkService")
networkService:FireAllClients("GameStateChanged", self.States.ROUND_ENDING, data)
task.delay(data and data.duration or 5, function()
if self._currentState == self.States.ROUND_ENDING then
self:TransitionToState(self.States.GAME_OVER, data)
end
end)
end
})
self:RegisterState({
name = self.States.GAME_OVER,
canEnterFrom = {self.States.ROUND_ENDING},
canExitTo = {self.States.INTERMISSION, self.States.LOBBY},
allowPlayerJoin = true,
onEnter = function(gameState, previousState, data)
-- Default game over behavior
local networkService = self._registry:GetService("NetworkService")
networkService:FireAllClients("GameStateChanged", self.States.GAME_OVER, data)
task.delay(data and data.duration or 8, function()
if self._currentState == self.States.GAME_OVER then
self:TransitionToState(self.States.INTERMISSION)
end
end)
end
})
self._initialized = true
return true
end
function GameStateService:Start(registry: ServiceRegistry): boolean
if not self._initialized then
error("GameStateService must be initialized before starting", 2)
end
if self._started then
return true
end
-- Set up update loop for state updates
self._updateConnection = game:GetService("RunService").Heartbeat:Connect(function(deltaTime)
if self._currentState then
local stateDefinition = self._stateDefinitions[self._currentState]
if stateDefinition and typeof(stateDefinition.update) == "function" then
stateDefinition.update(self, deltaTime)
end
end
end)
-- Start in the lobby state by default
self:TransitionToState(self.States.LOBBY, {initialState = true})
self._started = true
return true
end
function GameStateService:Stop(registry: ServiceRegistry): boolean
if not self._started then
return true
end
-- Disconnect update loop
if self._updateConnection then
self._updateConnection:Disconnect()
self._updateConnection = nil
end
-- Clear state
self._currentState = nil
self._previousState = nil
self._started = false
return true
end
-- Public API
function GameStateService:RegisterState(stateDefinition: GameStateDefinition)
assert(stateDefinition.name, "State must have a name")
self._stateDefinitions[stateDefinition.name] = stateDefinition
end
function GameStateService:GetCurrentState(): string?
return self._currentState
end
function GameStateService:GetCurrentStateMetadata(): GameStateMetadata?
if not self._currentState then
return nil
end
return self._stateMetadata[self._currentState]
end
function GameStateService:GetStateData(): any?
local metadata = self:GetCurrentStateMetadata()
return metadata and metadata.data
end
function GameStateService:TransitionToState(stateName: string, data: any?): boolean
if not self._stateDefinitions[stateName] then
warn(string.format("Attempted to transition to undefined state: %s", stateName))
return false
end
-- Check if we can transition from current state
if self._currentState then
local currentStateDefinition = self._stateDefinitions[self._currentState]
-- Check if the current state can exit to the target state
if currentStateDefinition.canExitTo and
not table.find(currentStateDefinition.canExitTo, stateName) then
warn(string.format("Cannot transition from %s to %s (not in canExitTo list)",
self._currentState, stateName))
return false
end
-- Check if the target state can be entered from the current state
local targetStateDefinition = self._stateDefinitions[stateName]
if targetStateDefinition.canEnterFrom and
not table.find(targetStateDefinition.canEnterFrom, self._currentState) then
warn(string.format("Cannot transition from %s to %s (not in canEnterFrom list)",
self._currentState, stateName))
return false
end
end
-- Exit current state
if self._currentState then
local currentStateDefinition = self._stateDefinitions[self._currentState]
if currentStateDefinition and typeof(currentStateDefinition.onExit) == "function" then
currentStateDefinition.onExit(self, stateName, data)
end
end
-- Update state information
self._previousState = self._currentState
self._currentState = stateName
-- Create new state metadata
self._stateMetadata[stateName] = {
name = stateName,
enterTime = os.clock(),
data = data
}
-- Enter new state
local newStateDefinition = self._stateDefinitions[stateName]
if newStateDefinition and typeof(newStateDefinition.onEnter) == "function" then
task.spawn(function()
newStateDefinition.onEnter(self, self._previousState, data)
end)
end
-- Notify listeners
for _, listener in ipairs(self._listeners) do
task.spawn(function()
listener(stateName, self._previousState, data)
end)
end
return true
end
function GameStateService:AddStateChangeListener(callback: GameStateTransitionEvent): number
table.insert(self._listeners, callback)
return #self._listeners
end
function GameStateService:RemoveStateChangeListener(listenerId: number)
if self._listeners[listenerId] then
self._listeners[listenerId] = nil
end
end
function GameStateService:IsPlayerJoinAllowed(): boolean
if not self._currentState then
return true -- Default to allowing joins if no state is set
end
local stateDefinition = self._stateDefinitions[self._currentState]
if not stateDefinition then
return true
end
return stateDefinition.allowPlayerJoin ~= false
end
function GameStateService:GetTimeSinceStateEntered(): number
if not self._currentState then
return 0
end
local metadata = self._stateMetadata[self._currentState]
if not metadata then
return 0
end
return os.clock() - metadata.enterTime
end
function GameStateService:GetStatus(): string
if not self._initialized then
return "UNINITIALIZED"
elseif not self._started then
return "INITIALIZED"
else
return "STARTED"
end
end
return GameStateService
Complete Game Setup with Service Registry
Here’s how you would set up a complete game using the Service Registry pattern:
--!strict
-- ServerMain.lua (to be placed in ServerScriptService)
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Load the Service Registry
local ServiceRegistry = require(ServerScriptService.Framework.ServiceRegistry)
local Types = require(ServerScriptService.Framework.Types)
-- Create the registry instance
local registry = ServiceRegistry.new()
-- Import services
local services = {
-- Core services
DataService = require(ServerScriptService.Services.DataService),
EventService = require(ServerScriptService.Services.EventService),
NetworkService = require(ServerScriptService.Services.NetworkService),
-- Game services
GameStateService = require(ServerScriptService.Services.GameStateService),
PlayerService = require(ServerScriptService.Services.PlayerService),
InventoryService = require(ServerScriptService.Services.InventoryService),
CombatService = require(ServerScriptService.Services.CombatService),
QuestService = require(ServerScriptService.Services.QuestService),
-- Utility services
PerformanceOptimizer = require(ServerScriptService.Services.PerformanceOptimizer)
}
-- Initialize all service instances
local serviceInstances = {}
for name, serviceModule in pairs(services) do
serviceInstances[name] = serviceModule.new()
end
-- Register services with dependencies
registry:RegisterService("EventService", serviceInstances.EventService, {
priority = 100, -- High priority - should initialize first
tags = {"Core"}
})
registry:RegisterService("NetworkService", serviceInstances.NetworkService, {
priority = 90,
tags = {"Core"}
})
registry:RegisterService("DataService", serviceInstances.DataService, {
priority = 80,
dependencies = {
{name = "EventService", required = true}
},
tags = {"Core", "Data"}
})
registry:RegisterService("PerformanceOptimizer", serviceInstances.PerformanceOptimizer, {
priority = 70,
tags = {"Core", "Utility"}
})
registry:RegisterService("GameStateService", serviceInstances.GameStateService, {
priority = 60,
dependencies = {
{name = "EventService", required = true},
{name = "NetworkService", required = true}
},
tags = {"Game"}
})
registry:RegisterService("PlayerService", serviceInstances.PlayerService, {
priority = 50,
dependencies = {
{name = "DataService", required = true},
{name = "EventService", required = true},
{name = "NetworkService", required = true},
{name = "GameStateService", required = true}
},
tags = {"Game", "Player"}
})
registry:RegisterService("InventoryService", serviceInstances.InventoryService, {
priority = 40,
dependencies = {
{name = "DataService", required = true},
{name = "EventService", required = true},
{name = "NetworkService", required = true},
{name = "PlayerService", required = true}
},
tags = {"Game", "Player"}
})
registry:RegisterService("CombatService", serviceInstances.CombatService, {
priority = 30,
dependencies = {
{name = "PlayerService", required = true},
{name = "InventoryService", required = true},
{name = "EventService", required = true},
{name = "NetworkService", required = true}
},
tags = {"Game", "Combat"}
})
registry:RegisterService("QuestService", serviceInstances.QuestService, {
priority = 20,
dependencies = {
{name = "DataService", required = true},
{name = "PlayerService", required = true},
{name = "EventService", required = true},
{name = "NetworkService", required = true},
{name = "CombatService", required = false} -- Optional dependency
},
tags = {"Game", "Quest"}
})
-- Enable debug mode for development
if game:GetService("RunService"):IsStudio() then
registry:EnableDebugMode(true)
end
-- Check for circular dependencies
local hasCycle, cyclePath = registry:CheckCircularDependencies()
if hasCycle then
error(string.format("Circular dependency detected: %s", table.concat(cyclePath or {}, " -> ")))
end
-- Initialize and start services
local initSuccess = registry:InitializeAllServices()
if not initSuccess then
error("Failed to initialize all services!")
end
local startSuccess = registry:StartAllServices()
if not startSuccess then
error("Failed to start all services!")
end
-- Enable performance profiling in development
if game:GetService("RunService"):IsStudio() then
local performanceOptimizer = registry:GetService("PerformanceOptimizer")
performanceOptimizer:EnableProfiling()
-- Add example throttling
performanceOptimizer:ThrottleServiceMethod("QuestService", "UpdateQuestProgress", {
maxCalls = 10,
interval = 1,
spreadCalls = true
})
end
-- Make the registry accessible to other scripts
_G.ServiceRegistry = registry
-- Log startup completion
print("Server initialized successfully!")
-- For debugging: Print diagnostic info
task.spawn(function()
while true do
task.wait(60)
local diagnosticInfo = registry:GetDiagnosticInfo()
print("== Service Registry Status ==")
print(string.format("Total Services: %d", diagnosticInfo.stats.totalServices))
print(string.format("Initialized: %d", diagnosticInfo.stats.initialized))
print(string.format("Started: %d", diagnosticInfo.stats.started))
print(string.format("Failed: %d", diagnosticInfo.stats.failed))
end
end)