Skip to main content

Clean Architecture Guide

This repository follows Clean Architecture principles as defined by Robert C. Martin (Uncle Bob).
This is a comprehensive guide to the architectural decisions and patterns used throughout the Send v5 codebase.

Core Principles

1. The Dependency Rule

Dependencies point inward toward business logic:
External Frameworks → Infrastructure → Use Cases → Entities
Never the reverse. Business logic has zero knowledge of databases, UI, or frameworks.

2. Screaming Architecture

The folder structure reveals what the application does, not what frameworks it uses:
  • /controllers, /models, /views (framework-centric)
  • /games/wheel/wheel-core/entities (domain-centric)

Repository Structure

/
├── games/                       # Monorepo for all game projects
│   ├── package.json            # npm workspaces configuration
│   └── wheel/
│       ├── wheel-core/         # Domain Layer (entities + use cases)
│       ├── wheel-infrastructure/ # Infrastructure Layer (repositories)
│       ├── wheel-dashboard/    # Presentation Layer (admin UI)
│       └── wheel-instances/    # Presentation Layer (public UI)
├── api/                        # Rust API workspace
│   ├── Cargo.toml             # Cargo workspace configuration
│   └── wheel/                 # Wheel API (Rust)
│       └── src/
│           ├── domain/        # Entities
│           ├── use_cases/     # Application logic
│           ├── adapters/      # HTTP handlers
│           └── infrastructure/ # Database repositories
├── dashboard/                  # Global dashboard (Next.js)
└── docs/                       # Mintlify documentation

Clean Architecture Layers

Layer 1: Entities (Domain)

Location: games/wheel/wheel-core/src/entities/, api/wheel/src/domain/ Purpose: Core business rules and domain logic Rules:
  • ✅ Pure business logic
  • ✅ Zero external dependencies
  • ✅ Framework-agnostic
  • ❌ No database imports
  • ❌ No UI imports
  • ❌ No HTTP imports
Example: Profile.create() validates business rules (name, version format, config hash)

Layer 2: Use Cases (Application Business Rules)

Location: games/wheel/wheel-core/src/use-cases/, api/wheel/src/use_cases/ Purpose: Application-specific business logic Rules:
  • ✅ Orchestrates entities
  • ✅ Depends on repository interfaces (not implementations)
  • ✅ Contains workflow logic
  • ❌ No framework dependencies
  • ❌ No database implementation details
Example: CreateProfileUseCase orchestrates profile creation, hashes config, saves to repository

Layer 3: Interface Adapters

Location:
  • Controllers: games/wheel/wheel-dashboard/src/app/, api/wheel/src/adapters/
  • Repositories: games/wheel/wheel-infrastructure/src/repositories/
Purpose: Convert data between use cases and external systems Rules:
  • ✅ Implements repository interfaces from domain
  • ✅ Converts HTTP/UI data to domain entities
  • ✅ Converts domain entities to HTTP/UI responses
  • ❌ No business logic (delegates to use cases)
Example: NileProfileRepository implements IProfileRepository interface

Layer 4: Frameworks & Drivers (Infrastructure)

Location: wheel-infrastructure/src/nile-client.ts, Next.js framework, Axum framework Purpose: External tools and frameworks Rules:
  • ✅ Database connections
  • ✅ Web frameworks
  • ✅ UI libraries
  • ✅ External APIs
  • ❌ No business logic

Testing Strategy

1. Unit Tests (Entities)

Location: wheel-core/src/entities/*.test.ts Test pure business logic:
  • Profile name validation
  • Version format validation
  • Config hash validation
  • Instance state transitions
  • Link slug generation
Dependencies: None (pure functions)

2. Use Case Tests

Location: wheel-core/src/use-cases/*.test.ts Test application workflows with mocked repositories.

3. Integration Tests

Location: wheel-infrastructure/src/repositories/*.test.ts Test repository implementations with test database.

4. E2E Tests

Location: wheel-dashboard/e2e/, wheel-instances/e2e/ Test full user workflows.

Adding New Features

Example: Add “Spin History” Feature

Step 1: Domain Layer (wheel-core)

// 1. Create entity
// wheel-core/src/entities/spin-history.entity.ts
export class SpinHistory {
  // Business rules: store spin results, enforce retention policy
}

// 2. Create repository interface
// wheel-core/src/repositories/spin-history.repository.ts
export interface ISpinHistoryRepository {
  save(history: SpinHistory): Promise<void>;
  findByInstance(instanceId: string): Promise<SpinHistory[]>;
}

// 3. Create use case
// wheel-core/src/use-cases/get-spin-history.use-case.ts
export class GetSpinHistoryUseCase {
  constructor(private repo: ISpinHistoryRepository) {}
  execute(instanceId: string) { /* ... */ }
}

Step 2: Infrastructure Layer (wheel-infrastructure)

// 4. Implement repository
// wheel-infrastructure/src/repositories/nile-spin-history.repository.ts
export class NileSpinHistoryRepository implements ISpinHistoryRepository {
  // Nile SDK implementation
}

Step 3: UI Layer (wheel-dashboard)

// 5. Create UI page
// wheel-dashboard/src/app/spin-history/page.tsx
import { GetSpinHistoryUseCase } from '@send/wheel-core';
// Use the use case to fetch and display data
Key: Business logic stays in wheel-core, UI just displays it.

Code Review Checklist

Before committing, verify:
  • No business logic in UI components
  • No database imports in wheel-core
  • Use cases depend on interfaces, not implementations
  • Entities have zero framework dependencies
  • Repository implementations live in wheel-infrastructure
  • Controllers convert data, don’t contain logic
  • All dependencies point inward

Learn More

  • Clean Architecture Book: Robert C. Martin
  • Domain-Driven Design: Eric Evans
  • SOLID Principles: Uncle Bob’s blog