Skip to main content
Architecture

Vertical slices, finally possible in a game client.

You have written the shop three times. Not because the rules changed — because in every project the shop was wired into that project's managers, save format and events, and nothing could be lifted out. A vertical slice — a feature folder — is the opposite: everything one feature needs, kept together, with the rest of the game reaching it only through declared references. Conjure makes that possible in a game client because a feature's state and rules live in one schema module.

What a slice is

One feature's data, rules, UI view and server behaviour in one folder.

What it is not

Not layers shared by every feature: UI, managers, services, data, each touched by every change.

Why it was hard in games

State lived in managers and singletons; nothing owned anything, so everything depended on everything.

interactive · four features, two shapes
UIManagersServicesDataShopPresenterQuestPanelBattlePassUIInventoryViewInventoryManagerShopManagerQuestManagerBattlePassManagerQuestSystemPassServiceShopServiceInventoryServicePassProgressQuestSaveInventoryDataShopDataa feature is a horizontal smear; removing one leaves holes in four layersUI viewmodulesystemsby commandby commandby idby commandShopShopViewbinds view modelmodule Shop {tablescommandsreactive query}InventoryInventoryViewbinds view modelmodule Inventory {tablescommandsreactive query}QuestsQuestViewbinds view modelmodule Quests {tablescommandsreactive query}systemsBattle passBattlePassViewbinds view modelmodule BattlePass {tablescommandsreactive query}systemsgenerated: storage · indexes · view models · server sideUIManagersServicesDataShopPresenterQuestPanelBattlePassUIInventoryViewInventoryManagerShopManagerQuestManagerBattlePassManagerQuestSystemPassServiceShopServiceInventoryServicePassProgressQuestSaveInventoryDataShopDataa feature is a horizontal smear;removing one leaves holes in four layersby commandby commandby idby commandShopShopViewbinds view modelmodule Shop {tablescommandsreactive query}InventoryInventoryViewbinds view modelmodule Inventory {tablescommandsreactive query}QuestsQuestViewbinds view modelmodule Quests {tablescommandsreactive query}systemsBattle passBattlePassViewbinds view modelmodule BattlePass {tablescommandsreactive query}systemsgenerated: storage · indexesview models · server side

7 cross-feature calls across 4 layers

Today: four layers, and every feature smeared across all of them, wired to the others at every layer. Removing one leaves holes in four layers.

One folder

What a slice contains

features/battlepass/  battlepass.conjuremodule BattlePass { … }  BattlePassView.csbinds the view model  BattlePassSystems.csoptional per-frame logic  art/prefabs, sprites

The feature's state, rules, UI view and frame logic sit together. Delete the folder and the feature is gone; copy it and the feature moves. A module groups the feature's tables, command and reactive query. The CLI generates an interface per module — IBattlePass for module BattlePass, as the docs show ICore for module Core — and the UI view binds the reactive query, its view model, through the generated context:

C#
var track = context.Track(playerId: player.Id);
passView.Bind(track.Current);
track.SubscribeChanged(changes => passView.Apply(changes));
schema
module BattlePass {
table Tier(persistence: local, type_id: 7101, schema_version: 1) {
Id: int @id
XpRequired: int
}
table Progress(persistence: local, type_id: 7102, schema_version: 1) {
PlayerId: int @id
Xp: int
ClaimedTier: int
}
type ClaimTierResult {
tier: int
}
command ClaimTier(playerId: int, tier: int) -> ClaimTierResult
kind: local
{
require @tier > 0 else InvalidTier
upsert Progress
| key { PlayerId = @playerId }
| set { ClaimedTier = @tier }
| returning { tier = ClaimedTier } into claimed
return { tier = claimed.Tier }
}
reactive query Track(playerId: int) -> Progress[] {
from Progress p
| filter p.PlayerId == @playerId
}
}
Ownership

Why it holds together without coupling

One owner per table

A module owns its tables. Other slices read through declared queries and joins, never through a manager reference.

Commands are the only writes

State changes go through the module's commands. There is no cross-feature method call to break when a slice changes.

UI views bind to view models

A UI view subscribes to the module's view model. No presenter knows about other features' events.

The same slice runs on the server

prototype validated

Commands are deterministic, so the module's logic is the server's logic too.

Reuse

Carrying a feature to the next game

  1. 01

    Copy the folder.

  2. 02

    Compile: unresolved references come back as diagnostics, not runtime surprises.

  3. 03

    Point its declarations at the new game's items and currencies; the compiler re-checks every join and regenerates what depends on them. Override only what the new game truly does differently.

What travels: the schema module, its generated code and the UI view logic. What you adapt: the references to the new game's model, then art, layout and balance.

Boundaries

What this is not

Not microservices

One process, one context on the device. Slices are compile-time ownership, not network boundaries.

Not runtime plugins

Nothing is loaded dynamically. Slices are compiled together; the compiler checks the whole.

Not free of design

A slice still needs a clean boundary. The compiler enforces ownership; you still choose it.

Start with one slice

Pick a feature you have rewritten twice. Declare it as a module, bind one UI view, and see what the compiler says about the rest.