The Service Registry Design Pattern in Roblox Luau: A Comprehensive Guide

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)
4 Likes