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<CreateLeagueViewModel> {
    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<CreateLeagueViewModel> {
    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.