Skip to main content

Clean Architecture

The Games API is built using Clean Architecture principles for maintainability, testability, and flexibility.

Core Principles

1. Dependency Rule

Dependencies point inward toward business logic:
HTTP Handlers → Use Cases → Entities
Business logic has zero dependencies on frameworks, databases, or UI.

2. Layers

Domain Layer (Entities)

Pure business rules with no external dependencies. Location: games/wheel/wheel-core/src/entities/
  • Profile - Validates names, versions, config hashes
  • Prize - Validates prize data and image URLs
  • Config - Content-addressed configurations with probability validation
  • Instance - Game instance lifecycle management
  • Link - Slug generation and validation

Application Layer (Use Cases)

Orchestrates business workflows. Location: games/wheel/wheel-core/src/use-cases/
  • CreateProfileUseCase - Profile creation with hash generation
  • LookupLinkUseCase - Slug → instance resolution
  • CompleteInstanceUseCase - Prize selection algorithm

Infrastructure Layer

Database adapters implementing domain interfaces. Location: games/wheel/wheel-infrastructure/src/repositories/
  • NileProfileRepository implements IProfileRepository
  • NilePrizeRepository implements IPrizeRepository
  • etc.

Presentation Layer

HTTP handlers (Rust + Axum). Location: api/wheel/src/adapters/ Converts HTTP requests → domain operations → HTTP responses

3. Benefits

Testability

Test business logic without frameworks:
// Pure unit test (< 1ms)
const profile = Profile.create('tenant', 'Summer Wheel', '1.0', hash);
expect(profile.name).toBe('Summer Wheel');
// No database, HTTP, or framework needed!

Flexibility

Swap infrastructure without changing business logic:
// Today: Nile
const repo = new NileProfileRepository();

// Tomorrow: Firebase (business logic unchanged!)
const repo = new FirebaseProfileRepository();

Independence

  • Framework changes don’t affect business logic
  • Database changes don’t affect use cases
  • UI changes don’t affect entities

Design Patterns

Repository Pattern

Domain defines interfaces, infrastructure implements:
// Domain (wheel-core)
interface IProfileRepository {
  findById(id: string): Promise<Profile | null>;
  create(profile: Profile): Promise<Profile>;
}

// Infrastructure (wheel-infrastructure)
class NileProfileRepository implements IProfileRepository {
  // Nile SDK implementation
}

Use Case Pattern

Each use case handles one workflow:
class CreateProfileUseCase {
  constructor(
    private profileRepo: IProfileRepository,
    private configRepo: IConfigRepository
  ) {}

  async execute(request: CreateProfileRequest) {
    // Orchestrate entities
    // Call repositories
    // Return response
  }
}

Config Deduplication

Configs are content-addressed and deduplicated:
  1. Client POSTs config data
  2. System generates SHA256 hash
  3. Check if hash exists in database
  4. If exists: return existing config ID
  5. If not: create new config with hash
This prevents duplicate configs and saves storage.

Folder Structure

games/wheel/
├── wheel-core/           # Domain + Use Cases (pure logic)
│   ├── entities/
│   ├── use-cases/
│   └── repositories/     # Interfaces only
├── wheel-infrastructure/ # Nile implementations
│   └── repositories/
├── wheel-dashboard/      # Admin UI
└── wheel-instances/      # Public UI

api/wheel/src/            # Rust API
├── domain/               # Rust entities
├── use_cases/
├── adapters/             # HTTP handlers
└── infrastructure/       # SQLx repos

Learn More