Clean Architecture for Roblox Studio
Type files define the shape of your data. They live in Shared/Types/ so both the server and client can import them. They are plain modules — no classes, no logic beyond a Default() function that returns a fresh table.
PlayerData is the only persistent data structure in this game. It holds the player’s coin balance.
-- Shared/Types/PlayerTypes.lua
--!nonstrict
--- @type PlayerData
--- The full persistent state for a single player.
--- { coins: number }
--- Returns a default PlayerData table for a new player.
--- @return PlayerData
local function Default()
return {
coins = 0,
}
end
return { Default = Default }
When a player joins for the first time, PlayerRepository:Load() returns nil (no existing data). The Service uses Default() as the fallback:
local data = self._repo:Load(player.UserId) or PlayerTypes.Default()
This ensures every player always has a valid data table in the cache, whether they are new or returning.
Later in the tutorial series, we will add an inventory field. When you need to extend PlayerData, add the field here and update Default():
local function Default()
return {
coins = 0,
inventory = {}, -- added later
}
end
Nothing else in the architecture changes. The Repository persists whatever is in the table. The Service mutates it. The Cubit reflects it.
Next: Step 3 — Repository