System Architecture
System architecture defines how you structure and organize your systems to create maintainable, scalable, and coherent applications. Good architecture makes your codebase easier to understand, modify, and extend as your application grows in complexity.
Architectural Principles
Separation of Concerns
Each system should have a clear, distinct responsibility that doesn't overlap with other systems. This makes your codebase more modular and easier to maintain.
// Good: Separated concerns
#[dojo::contract]
mod movement_system {
use starknet::{ContractAddress, get_caller_address};
use dojo::model::{ModelStorage};
use dojo::world::{WorldStorage, WorldStorageTrait};
fn move(ref self: ContractState, direction: Direction) { /* ... */ }
fn teleport(ref self: ContractState, target: Vec2) { /* ... */ }
}
#[dojo::contract]
mod combat_system {
use starknet::{ContractAddress, get_caller_address};
use dojo::model::{ModelStorage};
use dojo::world::{WorldStorage, WorldStorageTrait};
fn attack(ref self: ContractState, target: ContractAddress) { /* ... */ }
fn defend(ref self: ContractState) { /* ... */ }
}
#[dojo::contract]
mod inventory_system {
use starknet::{ContractAddress, get_caller_address};
use dojo::model::{ModelStorage};
use dojo::world::{WorldStorage, WorldStorageTrait};
fn pickup_item(ref self: ContractState, item_id: u32) { /* ... */ }
fn drop_item(ref self: ContractState, item_id: u32) { /* ... */ }
}
Dependency Direction
Systems should depend on abstractions (models and world interface) rather than concrete implementations. This creates a clean dependency flow and makes testing easier.
┌─────────────────────────────────────────────────┐
│ Dependency Flow │
├─────────────────────────────────────────────────┤
│ Systems → World Contract → Models/Events │
│ (Logic) (Interface) (Data) │
└─────────────────────────────────────────────────┘
Interface Segregation
Design focused interfaces that expose only what's necessary. Large, monolithic interfaces become difficult to implement and maintain.
// Good: Focused interfaces
#[starknet::interface]
trait IMovement<T> {
fn move(ref self: T, direction: Direction);
fn get_position(self: @T) -> Vec2;
}
#[starknet::interface]
trait ICombat<T> {
fn attack(ref self: T, target: ContractAddress);
fn defend(ref self: T);
}
// Bad: Monolithic interface
#[starknet::interface]
trait IGameActions<T> {
fn move(ref self: T, direction: Direction);
fn attack(ref self: T, target: ContractAddress);
fn craft_item(ref self: T, recipe: Recipe);
fn trade(ref self: T, other: ContractAddress);
// ... 20+ functions
}
Structural Patterns
Single System Per Contract
The simplest pattern: one system per contract. This provides maximum isolation and permission granularity.
// movement.cairo
#[dojo::contract]
mod movement {
use super::IMovement;
#[abi(embed_v0)]
impl MovementImpl of IMovement<ContractState> {
fn move(ref self: ContractState, direction: Direction) {
let mut world = self.world(@"game");
// Movement logic
}
}
}
// combat.cairo
#[dojo::contract]
mod combat {
use super::ICombat;
#[abi(embed_v0)]
impl CombatImpl of ICombat<ContractState> {
fn attack(ref self: ContractState, target: ContractAddress) {
let mut world = self.world(@"game");
// Combat logic
}
}
}
- Clear permission boundaries
- Easy to test and deploy independently
- Minimal complexity
- More contracts to manage
- Potential gas overhead for cross-system operations
Grouped Systems
Related systems grouped within a single contract. This pattern balances modularity with operational efficiency.
// player_actions.cairo
#[dojo::contract]
mod player_actions {
use super::{IMovement, ICombat, IInventory};
#[abi(embed_v0)]
impl MovementImpl of IMovement<ContractState> {
fn move(ref self: ContractState, direction: Direction) { /* ... */ }
}
#[abi(embed_v0)]
impl CombatImpl of ICombat<ContractState> {
fn attack(ref self: ContractState, target: ContractAddress) { /* ... */ }
}
#[abi(embed_v0)]
impl InventoryImpl of IInventory<ContractState> {
fn pickup_item(ref self: ContractState, item_id: u32) { /* ... */ }
}
}
- Shared permissions and internal helpers
- Fewer contracts to manage
- Efficient cross-system operations
- Larger contract size
- More complex testing
- Potential for tight coupling
Hierarchical Systems
Systems organized in a hierarchy, where higher-level systems coordinate lower-level ones.
// game_coordinator.cairo
#[dojo::contract]
mod game_coordinator {
use super::IGameCoordinator;
#[abi(embed_v0)]
impl CoordinatorImpl of IGameCoordinator<ContractState> {
fn process_turn(ref self: ContractState, player: ContractAddress) {
let mut world = self.world(@"game");
// Coordinate multiple subsystems
self.handle_movement(player);
self.handle_combat(player);
self.handle_resource_generation(player);
}
}
#[generate_trait]
impl InternalImpl of InternalTrait {
fn handle_movement(self: @ContractState, player: ContractAddress) { /* ... */ }
fn handle_combat(self: @ContractState, player: ContractAddress) { /* ... */ }
fn handle_resource_generation(self: @ContractState, player: ContractAddress) { /* ... */ }
}
}
- Clear system orchestration
- Simplified external interface
- Coordinated operations
- Potential single point of failure
- Complex internal logic
- Harder to extend independently
Permission Architecture
Granular Permissions
Assign permissions at the finest grain possible while maintaining operational efficiency.
// Each system gets specific permissions
[writers]
"game-Position" = ["game-movement"]
"game-Health" = ["game-combat"]
"game-Inventory" = ["game-inventory"]
// Avoid overly broad permissions
# Bad: Too broad
# "game" = ["game-player_actions"]
Permission Inheritance
Use namespace-level permissions for systems that need broad access.
// For systems that coordinate multiple models
[writers]
"game" = ["game-coordinator"] // Can write to all models in namespace
// For systems with specific access
"game-PlayerStats" = ["game-character_system"]
"game-MarketData" = ["game-trading_system"]
Permission Hierarchies
Design permission hierarchies that reflect your system architecture.
World Owner (Admin)
├── Namespace Owner (Game Admin)
│ ├── Coordinator System (Full namespace access)
│ ├── Movement System (Position models)
│ ├── Combat System (Health, Combat models)
│ └── Trading System (Inventory, Market models)
Modular Design
Core Systems
Identify and implement core systems that provide fundamental functionality.
// Core systems that most games need
mod core_systems {
pub mod identity; // Player registration and identity
pub mod movement; // Position and movement mechanics
pub mod ownership; // Asset and item ownership
pub mod permissions; // Access control and authorization
}
Domain Systems
Implement domain-specific systems that provide specialized functionality.
// Domain-specific systems
mod game_systems {
pub mod combat; // Fighting mechanics
pub mod crafting; // Item creation
pub mod trading; // Marketplace functionality
pub mod guilds; // Social organization
}
Extension Systems
Design extension points for adding new functionality without modifying core systems.
// Extension system interface
#[starknet::interface]
trait IExtension<T> {
fn process_event(ref self: T, event: GameEvent);
fn get_name(self: @T) -> ByteArray;
}
// Extensions can be added without modifying core systems
#[dojo::contract]
mod achievement_extension {
impl AchievementExtension of IExtension<ContractState> {
fn process_event(ref self: ContractState, event: GameEvent) {
// Handle achievement triggers
}
fn get_name(self: @ContractState) -> ByteArray {
"achievements"
}
}
}
Composability Patterns
Mixin Pattern
Use internal traits to compose functionality across systems.
#[generate_trait]
impl ValidationMixin of ValidationTrait {
fn validate_player_exists(self: @ContractState, player: ContractAddress) -> bool {
let mut world = self.world(@"game");
let player_data: Player = world.read_model(player);
player_data.exists
}
fn validate_position_in_bounds(self: @ContractState, pos: Vec2) -> bool {
pos.x < MAX_X && pos.y < MAX_Y
}
}
// Use in multiple systems
#[dojo::contract]
mod movement {
impl MovementImpl of IMovement<ContractState> {
fn move(ref self: ContractState, direction: Direction) {
let player = get_caller_address();
assert(self.validate_player_exists(player), 'Player not found');
// Movement logic
}
}
}
Trait Composition
Compose system behavior through trait implementations. Traits allow you to share common functionality across multiple systems.
// Define reusable utility traits
#[generate_trait]
impl ValidationMixin of ValidationTrait {
fn validate_player_exists(self: @ContractState, player: ContractAddress) -> bool {
let mut world = self.world(@"game");
let player_data: Player = world.read_model(player);
player_data.exists
}
fn validate_cooldown(self: @ContractState, last_action: u64, cooldown: u64) -> bool {
get_block_timestamp() >= last_action + cooldown
}
}
// Systems compose traits for shared functionality
#[dojo::contract]
mod movement {
impl MovementImpl of IMovement<ContractState> {
fn move(ref self: ContractState, direction: Direction) {
let player = get_caller_address();
// Use trait methods directly
assert(self.validate_player_exists(player), 'Player not found');
let mut player_data: Player = world.read_model(player);
assert(self.validate_cooldown(player_data.last_move, COOLDOWN), 'Move on cooldown');
// Movement logic...
}
}
}
#[dojo::contract]
mod combat {
impl CombatImpl of ICombat<ContractState> {
fn attack(ref self: ContractState, target: ContractAddress) {
let attacker = get_caller_address();
// Reuse the same validation logic
assert(self.validate_player_exists(attacker), 'Invalid attacker');
assert(self.validate_player_exists(target), 'Invalid target');
// Combat logic...
}
}
}
Evolution Strategies
Migration Patterns
Plan for system upgrades and data migration.
#[dojo::contract]
mod migration_system {
fn migrate_player_data(ref self: ContractState, player: ContractAddress) {
let mut world = self.world(@"game");
// Read old format
let old_data: PlayerV1 = world.read_model(player);
// Convert to new format
let new_data = PlayerV2 {
player: old_data.player,
level: old_data.level,
experience: old_data.experience,
// New fields with defaults
class: Class::Warrior,
specialization: Specialization::None,
};
// Save new state
world.erase_model(@old_data);
world.write_model(@new_data);
}
}
Backward Compatibility
Maintain compatibility with existing clients during upgrades.
#[starknet::interface]
trait IMovementV1<T> {
fn move(ref self: T, direction: Direction);
}
#[starknet::interface]
trait IMovementV2<T> {
fn move(ref self: T, direction: Direction);
fn move_with_modifiers(ref self: T, direction: Direction, modifiers: Span<Modifier>);
}
// Implement both interfaces for backward compatibility
#[dojo::contract]
mod movement_v2 {
#[abi(embed_v0)]
impl MovementV1 of IMovementV1<ContractState> {
fn move(ref self: ContractState, direction: Direction) {
// Call v2 implementation with empty modifiers
self.move_with_modifiers(direction, [].span());
}
}
#[abi(embed_v0)]
impl MovementV2 of IMovementV2<ContractState> {
fn move(ref self: ContractState, direction: Direction) {
self.move_with_modifiers(direction, [].span());
}
fn move_with_modifiers(ref self: ContractState, direction: Direction, modifiers: Span<Modifier>) {
// Full v2 implementation
}
}
}
Anti-Patterns
God Systems
Avoid systems that handle too many responsibilities.
// Bad: God system
#[dojo::contract]
mod game_system {
fn handle_everything(ref self: ContractState, action: GameAction) {
match action {
GameAction::Move(_) => { /* movement logic */ },
GameAction::Attack(_) => { /* combat logic */ },
GameAction::Craft(_) => { /* crafting logic */ },
GameAction::Trade(_) => { /* trading logic */ },
// ... 50+ different actions
}
}
}
Tight Coupling
Avoid systems that directly depend on each other's implementation details.
// Bad: Tight coupling
#[dojo::contract]
mod movement_system {
fn move(ref self: ContractState, direction: Direction) {
// Direct dependency on combat system internals
let combat_system = ICombatDispatcher { contract_address: COMBAT_ADDRESS };
combat_system.internal_update_position(new_position);
}
}
Shared Mutable State
Avoid systems that share mutable state outside of the world contract.
// Bad: Shared mutable state
static mut GAME_STATE: GameState = GameState::new();
#[dojo::contract]
mod system_a {
fn action_a(ref self: ContractState) {
unsafe {
GAME_STATE.modify(); // Dangerous shared state
}
}
}
System Discovery
Systems can be discovered through the world's DNS (Dojo Name System).
// Register a system with the world
fn register_system(ref self: ContractState, system_name: ByteArray, class_hash: ClassHash) {
let mut world = self.world(@"game");
// Register the system contract
world.register_contract(0, system_name, class_hash);
}
// Discover systems through DNS
fn find_system(self: @ContractState, system_name: ByteArray) -> Option<ContractAddress> {
let mut world = self.world(@"game");
if let Some((address, _)) = world.dns(@system_name) {
Option::Some(address)
} else {
Option::None
}
}
Best Practices
- Start Simple: Begin with single-system contracts and evolve as needed
- Plan Permissions: Design your permission model before implementing systems
- Test Boundaries: Ensure each system can be tested in isolation
- Document Interfaces: Clearly document what each system does and expects
- Version Thoughtfully: Plan for system evolution from the beginning
- Measure Performance: Monitor gas usage and optimize based on real data
Good system architecture is the foundation of maintainable Dojo applications. Take time to design your architecture thoughtfully - it will pay dividends as your world grows in complexity.
Next Steps
- System Coordination - Learn how systems interact and coordinate