The Architecture Model
A layered system with one absolute rule: dependencies always point inward.
The Core Idea
Clean Game Architecture is a layered system with one absolute rule: dependencies always point inward. The inner layers know nothing about the outer layers. The outer layers depend on the inner layers - never the reverse.
This is not a new concept. Robert C. Martin’s Clean Architecture has articulated this principle for decades. What’s different here is the adaptation to the specific reality of game development - where engines, real-time loops, UI frameworks, and network layers create unique pressures to couple everything together.
The Four Layers
Visualize four concentric rings. From inside out:

Core - The Heart of Your Game
The Core layer contains the domain: the entities, the rules, the state that makes your game your game. A player’s inventory. A combat formula. The rules of friendship. The state machine for a quest.
What belongs here:
- Entities and value objects (Player, Inventory, Friend, QuestState)
- Domain interfaces - the “ports” that define what the Core needs from the outside world (persistence, remote data) without knowing how those needs are fulfilled
- Pure game rules and state transitions
- Events that signal domain state changes
What does NOT belong here:
- Any engine type (no MonoBehaviour, no UObject, no Godot Node)
- Any UI concept
- Any network call or persistence mechanism
- Any framework dependency whatsoever
The Core is the most stable, most testable, most valuable part of your codebase. If you deleted every other layer, the Core would still compile. It would still be testable. It would still describe your game. For a deeper exploration of how to identify what belongs in the Core - including a useful thought experiment - see Finding Your Core.
Use Cases - The Feature’s API and Orchestration
The Use Cases layer serves two roles: it defines the public interface of a feature, and it contains the implementations that orchestrate business logic.
Use case interfaces are the feature’s public API - the only thing other features see. They describe what a feature can do: IClaimReward, ICheckQuestState, IAddFriend. These are resolved by other features via dependency injection.
Use case implementations contain the orchestration logic: “when the player claims a reward, validate eligibility, update inventory, notify analytics, trigger UI.” They coordinate the Core with the outer world, executing multi-step flows that may span multiple domain operations.
What also belongs here:
- Private interfaces for Controllers - these define what the Use Case needs from the presentation layers, but they live here, not in the outer layers
Key insight: The Use Cases layer defines what it needs from Controllers (via interfaces), but the Controllers implement those interfaces. The dependency points inward: Controllers depend on Use Case-defined contracts, not the other way around.
Think of it this way: if another feature needs to interact with your system, it depends on a use case interface. If a controller needs to drive a flow, it calls a use case implementation. Both live in this layer.
Controllers - The Bridge
The Controllers layer connects Use Cases to the outside world. Controllers react to user input, subscribe to domain events, and drive views - translating between the language of the domain and the language of the UI.
What belongs here:
- Controllers that implement interfaces defined by the Use Cases layer
- Logic for subscribing to Core events and updating views in response
- Input handling - translating player actions into use case calls
- Presentation logic that doesn’t belong in the domain (e.g., deciding which dialog to show, managing navigation)
Controllers know about Use Cases and Core (they depend inward), and they know about their Views (they depend outward to drive the UI). But they contain no engine-specific code themselves - they work through view interfaces, keeping presentation logic testable without the engine.
Services & Views - The Outside World
The outermost layer contains everything that touches external systems: the game engine, the UI framework, the network, the database, the file system.
What belongs here:
- Views - the actual engine-specific UI widgets and rendering
- Persistence adapters (saving/loading game state)
- Network/remote service adapters
- Audio, input, and platform-specific integrations
- Analytics instrumentation
This layer depends on everything inside it but nothing inside depends on it. The Core doesn’t know it exists. The Use Cases layer talks to it only through interfaces defined in Use Cases.
The Dependency Rule
The single most important principle:
Source code dependencies must point inward only.
An outer layer may reference, import, or depend on types from an inner layer. An inner layer must never reference types from an outer layer.

This means:
- Core never imports anything from Use Cases, Controllers, or Services & Views
- Use Cases never imports anything from Controllers or Services & Views
- Controllers never import anything from Services & Views
- Only Services & Views has full visibility of all layers
How Inner Layers Talk to Outer Layers
If the Core can’t reference Services, how does game state get saved? How does the UI get updated?
Two mechanisms:
1. Dependency Inversion (Ports)
The Core defines interfaces (ports) for what it needs - e.g., “I need something that can save player data.” The actual implementation lives in Services. At runtime, dependency injection wires the concrete implementation to the interface. The Core only knows the interface.
Core defines: IPersistence { void Save(PlayerData data); }
Services provides: LocalFilePersistence : IPersistence { ... }
CloudPersistence : IPersistence { ... }
The Core calls persistence.Save(data) without any knowledge of whether it’s writing to a local file, a cloud server, or an in-memory test double.
2. Events
The Core emits events when state changes. Outer layers subscribe to those events and react. The Core doesn’t know who’s listening or what they do with the information.
Core emits: OnInventoryChanged
Use Cases subscribe: Updates quest progress tracking
Controllers subscribe: Refreshes the inventory UI
Services subscribe: Triggers an analytics event
The Core simply announces that something happened. Everything else is someone else’s problem.
Why This Matters for Games
The Engine Becomes a Plugin
In a traditional game project, the engine is at the center. Everything depends on it. Your game logic is entangled with MonoBehaviours, Actors, or Nodes. Changing anything means fighting the engine.
With Clean Game Architecture, the engine lives in the outermost layer. Your game logic has zero engine dependencies. The engine is just one adapter among many - a way to render things on screen and handle input.
This doesn’t mean you’ll switch engines. But the discipline of keeping the engine at the periphery forces clean design decisions throughout the entire codebase. For a deeper exploration of how the engine relates to game logic - including a useful analogy to web browsers and the DOM - see The Game Object Model.
Features Become Independent
Each feature is a self-contained slice through the layers. The inventory system has its own Core, its own Use Cases, its own Controllers, its own Views. Features interact through Use Case interfaces, never by reaching into each other’s internals.
Feature A can depend on Feature B’s Use Cases - its public API. It never touches Feature B’s Controllers, Services or Views directly. For a complete walkthrough of how this looks in practice - folder structure, code, and runtime flow - see Vertical Slice Example.
Testing Becomes Trivial
Because the Core has zero external dependencies, you can test game rules in a plain test harness without launching the engine. Because Use Cases talk to Controllers through interfaces, you can substitute test doubles. Because features interact through Use Case interfaces, you can test features in isolation.
This is not a minor benefit. In most game projects, testing means “launch the game and click around.” With Clean Game Architecture, you can verify complex game logic in milliseconds.
Applying This in Practice
The theory is straightforward. The challenge is discipline - maintaining the dependency rule under the constant pressure of deadlines, prototyping, and “I’ll fix it later.”
The following topics explore the practical toolkit for making this work:
- Technical Toolkit - why loose coupling is the prerequisite and what tools enable it
- Dependency Injection - the primary mechanism for wiring layers together
- Event Systems - how inner layers communicate outward without coupling
- Asynchronous Flows - how to manage time and lifecycle safety
- Unit Tests - how clean layers enable testing that was previously impossible
- Architectural Tests - automated enforcement so the rules don’t erode
- Vertical Slice Example - organizing code by feature and seeing all layers work together end-to-end
To understand what happens without this discipline, see Difficulties of Game Architecture.
If you’re wondering whether this applies to your specific type of game - especially if your gameplay relies heavily on physics, real-time input, or engine-driven mechanics - see Finding Your Core for a genre-by-genre breakdown.
If you’re working with an existing codebase and wondering how to introduce this architecture incrementally, see Transition from Legacy for the migration strategy - both technical and organizational.
If you’re concerned about the performance cost of these layers - allocations, indirection, GC pressure - see The Performance Dilemma for an honest accounting of what this architecture costs at runtime and how Data-Oriented Design fits cleanly behind architectural boundaries.