Skip to content

Dojo Framework Overview

What is Dojo?

Dojo is a comprehensive framework for building provable games and autonomous worlds on Starknet. It combines the power of Cairo smart contracts with Entity-Component-System (ECS) architecture to create scalable, composable onchain applications.

Key Benefits:
  • Provable: All game logic and state changes are verifiable onchain
  • Composable: Modular design allows for easy extension and integration
  • Scalable: Optimized for high-performance onchain applications
  • Developer-friendly: Rich tooling and familiar development patterns

Understanding ECS Architecture

Entity-Component-System (ECS) is a design pattern that separates data from logic, enabling highly modular and scalable applications.

The Problem ECS Solves: Traditional object-oriented programming can lead to complex inheritance hierarchies and tight coupling between data and behavior. ECS addresses these issues by decomposing your application into three distinct parts:

The ECS Trinity

┌─────────────────────────────────────────────────────────────┐
│                        ECS PATTERN                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ENTITIES         COMPONENTS           SYSTEMS              │
│  (What)           (Data)               (Logic)              │
│                                                             │
│  Player           Position: {x, y}     Movement             │
│  Enemy            Health: {hp}         Combat               │
│  Item             Inventory: {items}   Crafting             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
  • Entities: The objects in your game — characters, items, etc.
  • Components: The properties of your entities — position, durability, etc.
  • Systems: The rules that govern your game — movement, combat, etc.

With ECS, you assign components to entities, and operate on them using systems. This approach allows for a more modular and scalable design, as well as better performance and memory usage. Entities are typically represented as a unique identifier, to which components are assigned. Systems can then operate over large numbers of entities at once, depending on the components they have.

As an example, a combat system might reduce the durability of all weapons used in a battle, while a rest system might increase the health of all members of a party.

ECS Benefits:
  • Modularity: Components can be mixed and matched across entity types
  • Performance: Systems can efficiently process large numbers of entities
  • Flexibility: Easy to add new components or systems without affecting existing code

Real-World Example

Consider a simple RPG where both players and monsters can move and fight:

// Components (Data)
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Position {
    #[key]
    entity_id: u32,
    x: u32,
    y: u32,
}
 
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Health {
    #[key]
    entity_id: u32,
    hp: u32,
    max_hp: u32,
}
 
// Systems (Logic)
#[starknet::interface]
trait IGameSystem<T> {
    fn move(ref self: T, entity_id: u32, direction: Direction);
    fn fight(ref self: T, attacker_id: u32, target_id: u32);
}
 
#[dojo::contract]
mod game_system {
    use super::{Position, Health, Direction, IGameSystem};
    use dojo::model::{ModelStorage};
    use dojo::world::{WorldStorage, WorldStorageTrait};
 
    #[abi(embed_v0)]
    impl GameSystemImpl of IGameSystem<ContractState> {
        fn move(ref self: ContractState, entity_id: u32, direction: Direction) {
            let mut world = self.world(@"my_game");
            let mut position: Position = world.read_model(entity_id);
 
            // Update position based on direction
 
            world.write_model(@position);
        }
 
        fn fight(ref self: ContractState, attacker_id: u32, target_id: u32) {
            let mut world = self.world(@"my_game");
            let mut attacker_health: Health = world.read_model(attacker_id);
            let mut target_health: Health = world.read_model(target_id);
 
            // Handle combat logic
 
            world.write_model(@target_health);
        }
    }
}

Both player and monsters can have Position and Health components, and the same systems work for both.

Dojo's ECS Implementation

Dojo adapts ECS for the onchain environment with three core concepts:

1. Models (Components)

Models are Cairo structs that define your application's data structures. They act as components in the ECS pattern.

#[derive(Copy, Drop, Serde)]
#[dojo::model] // Defines a model, used to generate Torii events
struct Position {
    #[key] // Defines a model's key, like an ORM primary key
    pub player: ContractAddress,
    // The rest of the model's attributes
    pub x: u32,
    pub y: u32,
}
Key Features:
  • Automatic indexing: All model changes are automatically indexed by Torii
  • Type safety: Full Cairo type system support
  • Composability: Models can be reused across different entity types

2. Systems (Smart Contracts)

Systems implement your application's business logic as Cairo contract functions.

// First define the interface
#[starknet::interface]
trait IActions<T> {
    fn move_player(ref self: T, direction: Direction);
}
 
#[dojo::contract] // Defines a Dojo contract
mod actions {
    use super::IActions;
    use starknet::{ContractAddress, get_caller_address};
    use dojo::model::{ModelStorage};
    use dojo::world::{WorldStorage, WorldStorageTrait};
 
    #[abi(embed_v0)]
    impl ActionsImpl of IActions<ContractState> {
        fn move_player(ref self: ContractState, direction: Direction) {
            // Get the world and player entity key
            let mut world = self.world(@"my_game");
            let player = get_caller_address();
 
            // Read current position
            let mut position: Position = world.read_model(player);
 
            // Update position based on direction
            match direction {
                Direction::Up => position.y += 1,
                Direction::Down => position.y -= 1,
                Direction::Left => position.x -= 1,
                Direction::Right => position.x += 1,
            }
 
            // Write updated position
            world.write_model(@position);
        }
    }
}
Key Features:
  • Permissioned access: Fine-grained control over who can modify what
  • Atomic operations: All state changes happen in a single transaction
  • Event emission: Automatic event generation for state changes

3. World (Resource Registry)

The World contract serves as the central coordinator, managing all models and systems.

// The World provides a unified interface for all operations
let mut world = self.world(@"my_game");
 
// Cairo's typing tells us we are querying the player's position
let position: Position = world.read_model(player_id);
 
// Update game state
world.write_model(@new_position);
 
// Emit custom events if needed
world.emit_event(@PlayerMoved { player, direction });
Key Features:
  • Centralized state: Single source of truth for all application data
  • Permission management: Hierarchical authorization system
  • Upgradeability: Safe model and system upgrades
  • Introspection: Rich metadata and schema information

Architecture Overview

world-map

Development Workflow

  1. Design Models: Define your data structures as Dojo models
  2. Implement Systems: Create contract functions that operate on models
  3. Configure Permissions: Set up authorization for your systems
  4. Deploy to World: Register your models and systems
  5. Build Client: Use indexed data to create rich user experiences

Best Practices

Model Design

  • Keep models small and focused - Each model should represent a single concept
  • Design for reusability - Models should be composable across different entity types
  • Plan for upgrades - Consider future schema changes when designing models
  • Always derive required traits - All models must derive Drop and Serde
  • Use key fields correctly - All #[key] fields must come before non-key fields

System Design

  • Minimize gas costs - Batch operations and optimize storage access
  • Handle errors gracefully - Use assertions and proper error handling
  • Design for permissions - Group related systems by permission requirements
  • Define interfaces first - Always create a #[starknet::interface] before implementing systems
  • Use proper mutability - Make world reference mutable when writing models

Architecture

  • Separate concerns - Keep data (models) and logic (systems) distinct
  • Use events effectively - Emit events for important state changes
  • Plan your namespaces - Organize resources logically and use consistent namespace strings
  • Follow ECS principles - Compose entities using multiple small, focused models

Next Steps

Ready to start building with Dojo? Here's your learning path: