Skip to content

Services

Services are single-instanced modules (singletons) that represent game features that you really only need one of.

Controllers?

Unlike other frameworks, there is no concept of "client controllers." Instead, services can be placed on both the server and client.

Nevertheless, it would still be a good idea to enforce a convention to name clientside services as "Controllers" for clarity.

Creating a simple service

First, let's define a simple service.

src/server/GreetService.lua
--@service
local service = {}

function service._init()
    print("Hello World!")
end

function service.greet(name: string)
    print("Hello " .. name .. "!")
end

return service

Warning

Do not place blocking or heavy code inside of a service's _init() method as it yeilds before the next service can start. Instead, you should use the Roblox task.spawn or task.defer functions. Also note that this method along with any annotated methods should be defined with . instead of :.

Dependency injection

This project aims to reduce the need for manual requires as much as possible. Thus, the preferred way to utilize services is through dependency injection (DI). It is a technique where an object recieves dependencies externally rather than internally.

In this case, the runtime loader injects other services into the constructors of services that require them. Utilizing this would decouple your game features and thus allow for modular code, easier testing, and easier refactors.

Let's create a new service that utilizes the greet() method of the above GreetService.

src/server/PlayerService.lua
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")

--@service, depends=[GreetService]
local service = {}

function service._init(deps)
    service.deps = deps

    Players.PlayerAdded:Connect(function(player)
        deps.GreetService.greet(player.Name)
    end)
end


return service

In this example:

  • The depends argument accepts a list of service names to inject.
  • The deps parameter is a table which contains the injected services at runtime.
  • We store the deps object inside the module so that we could access it in other functions.
  • We import the autogenerated type file for the service which contains the injected dependency list type.

Note

The build tool will automatically determine the load order of services based on injected dependencies!

@initService

This annotation basically allows you to use a function as a service. It's great for any one-time loaders which require dependencies.

It's effectively the same as defining a service with only an _init() method.

local m = {}

--@initService, depends=[GreetService]
function m.greetPlayer(deps)
    Players.PlayerAdded:Connect(function(player)
        deps.GreetService.greet(player.Name)
    end)
end

return m

@dependency

This is a simple annotation which @service inherits from. Modules annotated with it are not loaded automatically at runtime, ie they have no _init method. Use this for pure data modules which still need DI.