Clean Architecture – Application Services, Use Cases, Ports, and Data Flow (Strict, Final) This document defines the final, non-ambiguous Clean Architecture setup for the project. It explicitly covers: • Use Cases vs Application Services • Input & Output Ports (and what does not exist) • API responsibilities • Frontend responsibilities • Naming, placement, and dependency rules • End-to-end flow with concrete paths and code examples There are no hybrid concepts, no overloaded terms, and no optional interpretations. ⸻ 1. Architectural Layers (Final) Domain → Business truth Application → Use Cases + Application Services Adapters → API, Persistence, External Systems Frontend → UI, View Models, UX logic Only dependency-inward is allowed. ⸻ 2. Domain Layer (Core / Domain) What lives here • Entities (classes) • Value Objects (classes) • Domain Services (stateless business logic) • Domain Events • Domain Errors / Invariants What NEVER lives here • DTOs • Models • Ports • Use Cases • Application Services • Framework code ⸻ 3. Application Layer (Core / Application) The Application Layer has two distinct responsibilities: 1. Use Cases – business decisions 2. Application Services – orchestration of multiple use cases ⸻ 4. Use Cases (Application / Use Cases) Definition A Use Case represents one business intent. Examples: • CreateLeague • ApproveSponsorship • CompleteDriverOnboarding Rules • A Use Case: • contains business logic • enforces invariants • operates on domain entities • communicates ONLY via ports • A Use Case: • does NOT orchestrate multiple workflows • does NOT know HTTP, UI, DB, queues Structure core/racing/application/use-cases/ └─ CreateLeagueUseCase.ts Example export class CreateLeagueUseCase { constructor( private readonly leagueRepository: LeagueRepositoryPort, private readonly output: CreateLeagueOutputPort ) {} execute(input: CreateLeagueInputPort): void { // business rules & invariants const league = League.create(input.name, input.maxMembers); this.leagueRepository.save(league); this.output.presentSuccess(league.id); } } ⸻ 5. Ports (Application / Ports) The Only Two Kinds of Ports Everything crossing the Application boundary is a Port. Input Ports Input Ports describe what a use case needs. export interface CreateLeagueInputPort { readonly name: string; readonly maxMembers: number; } Rules: • Interfaces only • No behavior • No validation logic ⸻ Output Ports Output Ports describe how a use case emits outcomes. export interface CreateLeagueOutputPort { presentSuccess(leagueId: string): void; presentFailure(reason: string): void; } Rules: • No return values • No getters • No state • Use methods, not result objects ⸻ 6. Application Services (Application / Services) Definition An Application Service orchestrates multiple Use Cases. It exists because: • No single Use Case should know the whole workflow • Orchestration is not business logic Rules • Application Services: • call multiple Use Cases • define execution order • handle partial failure & compensation • Application Services: • do NOT contain business rules • do NOT modify entities directly Structure core/racing/application/services/ └─ LeagueSetupService.ts Example (with Edge Cases) export class LeagueSetupService { constructor( private readonly createLeague: CreateLeagueUseCase, private readonly createSeason: CreateSeasonUseCase, private readonly assignOwner: AssignLeagueOwnerUseCase, private readonly notify: SendLeagueWelcomeNotificationUseCase ) {} execute(input: LeagueSetupInputPort): void { const leagueId = this.createLeague.execute(input); try { this.createSeason.execute({ leagueId }); this.assignOwner.execute({ leagueId, ownerId: input.ownerId }); this.notify.execute({ leagueId, ownerId: input.ownerId }); } catch (error) { // compensation / rollback logic throw error; } } } Edge cases that belong ONLY here: • Partial failure handling • Workflow order • Optional steps • Retry / idempotency logic ⸻ 7. API Layer (apps/api) Responsibilities • Transport (HTTP) • Validation (request shape) • Mapping to Input Ports • Calling Application Services • Adapting Output Ports Structure apps/api/leagues/ ├─ LeagueController.ts ├─ presenters/ │ └─ CreateLeaguePresenter.ts └─ dto/ ├─ CreateLeagueRequestDto.ts └─ CreateLeagueResponseDto.ts ⸻ API Presenter (Adapter) export class CreateLeaguePresenter implements CreateLeagueOutputPort { private response!: CreateLeagueResponseDto; presentSuccess(leagueId: string): void { this.response = { success: true, leagueId }; } presentFailure(reason: string): void { this.response = { success: false, errorMessage: reason }; } getResponse(): CreateLeagueResponseDto { return this.response; } } ⸻ 8. Frontend Layer (apps/website) The frontend layer contains UI-specific data shapes. None of these cross into the Core. There are three distinct frontend data concepts: 1. API DTOs (transport) 2. Command Models (user input / form state) 3. View Models (UI display state) ⸻ 8.1 API DTOs (Transport Contracts) API DTOs represent exact HTTP contracts exposed by the backend. They are usually generated from OpenAPI or manually mirrored. apps/website/lib/dtos/ └─ CreateLeagueResponseDto.ts Rules: • Exact mirror of backend response • No UI logic • No derived values • Never used directly by components ⸻ 8.2 Command Models (User Input / Form State) Command Models represent user intent before submission. They are frontend-only and exist to manage: • form state • validation feedback • step-based wizards They are NOT: • domain objects • API DTOs • View Models apps/website/lib/commands/ └─ CreateLeagueCommandModel.ts Rules: • Classes (stateful) • May contain client-side validation • May contain UX-specific helpers (step validation, dirty flags) • Must expose a method to convert to an API Request DTO Example responsibility: • hold incomplete or invalid user input • guide the user through multi-step flows • prepare data for submission Command Models: • are consumed by components • are passed into services • are never sent directly over HTTP ⸻ 8.3 View Models (UI Display State) View Models represent fully prepared UI state after data is loaded. apps/website/lib/view-models/ └─ CreateLeagueViewModel.ts Rules: • Classes only • UI logic allowed (formatting, labels, derived flags) • No domain logic • No mutation after construction ⸻ 8.4 Website Presenters (DTO → ViewModel) Website Presenters are pure mappers. export class CreateLeaguePresenter { present(dto: CreateLeagueResponseDto): CreateLeagueViewModel { return new CreateLeagueViewModel(dto); } } Rules: • Input: API DTOs • Output: View Models • No side effects • No API calls ⸻ 8.5 Website Services (Orchestration) Website Services orchestrate: • Command Models • API Client calls • Presenter mappings export class LeagueService { async createLeague(command: CreateLeagueCommandModel): Promise { const dto = await this.api.createLeague(command.toRequestDto()); return this.presenter.present(dto); } } Rules: • Services accept Command Models • Services return View Models • Components never call API clients directly ⸻ View Models (UI State) apps/website/lib/view-models/ └─ CreateLeagueViewModel.ts export class CreateLeagueViewModel { constructor(private readonly dto: CreateLeagueResponseDto) {} get message(): string { return this.dto.success ? 'League created successfully' : this.dto.errorMessage ?? 'Creation failed'; } } Rules: • Classes only • UI logic allowed • No domain logic ⸻ Website Presenter (DTO → ViewModel) export class CreateLeaguePresenter { present(dto: CreateLeagueResponseDto): CreateLeagueViewModel { return new CreateLeagueViewModel(dto); } } ⸻ Website Service (Orchestration) export class LeagueService { constructor( private readonly api: LeaguesApiClient, private readonly presenter: CreateLeaguePresenter ) {} async createLeague(input: unknown): Promise { const dto = await this.api.createLeague(input); return this.presenter.present(dto); } } ⸻ 9. Full End-to-End Flow (Final) UI Component → Website Service → API Client → HTTP Request DTO → API Controller → Application Service → Use Case(s) → Domain → Output Port → API Presenter → HTTP Response DTO → Website Presenter → View Model → UI ⸻ 10. Final Non-Negotiable Rules • Core knows ONLY Ports + Domain • Core has NO Models, DTOs, or ViewModels • API talks ONLY to Application Services • Controllers NEVER call Use Cases directly • Frontend Components see ONLY View Models • DTOs never cross into UI components ⸻ 11. Final Merksatz Use Cases decide. Application Services orchestrate. Adapters translate. UI presents. If a class violates more than one of these roles, it is incorrectly placed.