Rogen IconRogen
Architectures

Layered

VS Code's layers, with feature folders inside them.

Feature folders, stacked in layers. Each layer may only require the layers below it, so the direction of every dependency is visible from the path.

The Rule

The top level has five layers. Each one holds modules, or feature folders with Server/, Client/ and Shared/ inside:

LayerHoldsMay require
basePure utilities that know nothing about the game: Signal, Promisebase
platformWrappers around Roblox and infrastructure: networking, data storesbase, platform
coreGame rules most features build on: combat, charactersbase, platform, core
featuresGameplay features: inventory, trading, questsbase, platform, core
appThe entry points: one Script and one LocalScripteverything

Features don't require each other. When two features need the same thing, it moves down into core.

Inside every feature, the sides follow one more rule:

  • Shared requires only Shared.
  • Server and Client require Shared, never each other.

Where It Comes From

  • VS Code. Its source code organization splits the code into layers (base, platform, editor, workbench, and code as the entry point that "stitches everything together"), each depending only on the layers below. "Inside each layer the code is organised by the target runtime environment", and workbench features live in contrib/<feature>/, with no dependency from outside contrib into it. VS Code checks its runtime rules with an ESLint rule of its own, code-layering.
  • Game engines. Jason Gregory's Game Engine Architecture (CRC Press) describes a runtime engine as layers, from the platform independence layer up through core systems to gameplay foundations and game-specific code. On Roblox, the engine layers are Roblox. What's left for a game is the top of that stack, which is this page's platform, core and features.
  • Unreal's Lyra. Epic's Lyra sample keeps a small, generic core and ships gameplay as Game Feature plugins, "fully encapsulated within the plugin". That's the core and features split.
  • Roblox. Script locations recommends "a single entry point on the client and server sides". That's app.

On Disk

File System
src
app
Client
main.client.luau
Server
main.server.luau
base
Promise.luau
Signal.luau
core
Combat
Client
CombatController.luau
Server
CombatService.luau
Shared
DamageTypes.luau
features
Inventory
Client
(Ui)
InventoryPanel.luau
InventoryController.luau
Server
InventoryService.luau
Shared
InventoryTypes.luau
platform
Data
Server
PlayerDataStore.luau
Network
Client
ClientNetwork.luau
Server
ServerNetwork.luau
Shared
Remotes.luau

base/ has no routing folder, because everything in it runs on both sides. app/ holds nothing but the two entry points.

The Config

The routes rogen init writes are enough:

default.rogen.json
{
	"$schema": "https://ldgerrits.github.io/rogen/schema/2/rogen.json",
	"rootDirs": ["src"],
	"routes": {
		"Server": "ServerScriptService",
		"Client": "StarterPlayer/StarterPlayerScripts",
		"Shared": "ReplicatedStorage/Shared",
		"*": "ReplicatedStorage/Shared"
	}
}

base/ matches no route, so the * route sends it to ReplicatedStorage/Shared. That's the Roblox version of VS Code's rule that base code runs everywhere.

Dropping a Layer From Studio

The layers show up in Studio, as in ServerScriptService/features/Inventory. To keep a layer on disk only, write it as an invisible folder. With every layer invisible:

src/(base)/Signal.luau                                 ->  ReplicatedStorage/Shared/Signal
src/(features)/Inventory/Server/InventoryService.luau  ->  ServerScriptService/Inventory/InventoryService
src/(app)/Server/main.server.luau                      ->  ServerScriptService/main

Invisible layers share one namespace

Two layers can then produce the same instance. A Combat folder in both (core) and (features) becomes one Combat folder in Studio, and a module with the same name in both is a clash: Rogen warns, and the last one wins. Keep layers visible if feature names repeat across them.

In Studio

Roblox Studio
ReplicatedStorage
Shared
base
Promise
Signal
core
Combat
DamageTypes
features
Inventory
InventoryTypes
platform
Network
Remotes
ServerScriptService
app
main
core
Combat
CombatService
features
Inventory
InventoryService
platform
Data
PlayerDataStore
Network
ServerNetwork
StarterPlayer
StarterPlayerScripts
app
main
core
Combat
CombatController
features
Inventory
InventoryController
InventoryPanel
platform
Network
ClientNetwork

app/Server/main.server.luau becomes a Script named main, and app/Client/main.client.luau a LocalScript. The routing folder governs them, so .server and .client keep Rojo's meaning.

Why It Fits a Game

  • The server/client problem is VS Code's problem. VS Code splits by runtime so that code for one environment never calls another's APIs. That's the rule between Server and Client, and Roblox already enforces half of it: a client can't require anything in ServerScriptService.
  • It scales with the team. Someone working on features/Trading can read core and platform without worrying about the other features, because nothing in features requires another feature. A pull request that stays inside features/Trading can't break the rest of the game.
  • One entry point per side. app/ is the only place scripts start, as Roblox recommends, so everything else is a ModuleScript that can be required, tested or disabled on its own.
  • Live games change their feature set. New modes and events are new folders in features/, on top of a core that changes less often.

Trade-offs

  • More folders. A small game doesn't need five layers. Start with feature folders and add layers when utilities and infrastructure start piling up.
  • Every module needs a layer. Deciding between core and features, or platform and base, is a judgement call. Write down what each layer means for your game.
  • The rules need a linter. Rogen doesn't read requires, so a features module that requires another feature still builds. In roblox-ts, ESLint can enforce every rule in the table; in Luau there's no common tool yet. See Enforcing the Rules.
  • Longer paths in Studio, unless you make the layers invisible, which trades the paths for possible name clashes.

Feature-Sliced Design

Feature-Sliced Design is the same idea as popularised in frontend development, with one extra rule. Its layers are app, pages, widgets, features, entities and shared, each split into slices, and:

  • "A module (file) in a slice can only import other slices when they are located on layers strictly below."
  • "Slices cannot use other slices on the same layer."

That second rule is what makes it stricter. The layout above lets a module require others in its own layer (core requires core); FSD never allows a sideways require, except in app and shared, which have no slices. For a game, pages becomes screens or modes, widgets holds HUD pieces, and each slice gets routing folders:

File System
src
app
Client
Server
entities
Item
Server
ItemRegistry.luau
Shared
ItemTypes.luau
features
Equip
Trade
shared
Remotes.luau
Signal.luau
widgets
Hotbar
Client
Hotbar.luau

shared is a route key

FSD's shared layer has the same name as the Shared route, so it's a routing folder: its modules land directly in ReplicatedStorage/Shared, and a Server/ folder inside it is ignored, because the outer route governs. Server-only code in shared/ would be sent to every client. Keep shared/ for code both sides run, or give the layer another name.

Pick FSD over the layout above when your team wants the stricter rule, or already knows it from the web.

On this page