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:
| Layer | Holds | May require |
|---|---|---|
base | Pure utilities that know nothing about the game: Signal, Promise | base |
platform | Wrappers around Roblox and infrastructure: networking, data stores | base, platform |
core | Game rules most features build on: combat, characters | base, platform, core |
features | Gameplay features: inventory, trading, quests | base, platform, core |
app | The entry points: one Script and one LocalScript | everything |
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:
Sharedrequires onlyShared.ServerandClientrequireShared, never each other.
Where It Comes From
- VS Code. Its source code organization splits the code into layers (
base,platform,editor,workbench, andcodeas 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 incontrib/<feature>/, with no dependency from outsidecontribinto 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,coreandfeatures. - 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
coreandfeaturessplit. - Roblox. Script locations recommends "a single entry point on the client and server sides". That's
app.
On Disk
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:
{
"$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/mainInvisible 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
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
ServerandClient, and Roblox already enforces half of it: a client can't require anything inServerScriptService. - It scales with the team. Someone working on
features/Tradingcan readcoreandplatformwithout worrying about the other features, because nothing infeaturesrequires another feature. A pull request that stays insidefeatures/Tradingcan'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 aModuleScriptthat 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
coreandfeatures, orplatformandbase, 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
featuresmodule 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:
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.