Use Case Architecture Guide This document defines the correct structure and responsibilities of Application Use Cases according to Clean Architecture, in a NestJS-based system. The goal is: • strict separation of concerns • correct terminology (no fake "ports") • minimal abstractions • long-term consistency This is the canonical reference for all use cases in this codebase. ~ 1. Core Concepts (Authoritative Definitions) Use Case • Encapsulates application-level business logic • Is the Input Port • Is injected via DI • Knows no API, no DTOs, no transport • Coordinates domain objects and infrastructure The public execute() method is the input port. ~ Input • Pure data • Not a port • Not an interface • May be omitted if the use case has no parameters type GetSponsorsInput = { leagueId: LeagueId } ~ Result • The business outcome of a use case • May contain Entities and Value Objects • Not a DTO • Never leaves the core directly type GetSponsorsResult = { sponsors: Sponsor[] } ~ Output Port • A behavioral boundary • Defines how the core communicates outward • Never a data structure • Lives in the Application Layer export interface UseCaseOutputPort { present(data: T): void } ~ Presenter • Implements UseCaseOutputPort • Lives in the API / UI layer • Translates Result → ViewModel / DTO • Holds internal state • Is pulled by the controller after execution ~ 2. Canonical Use Case Structure Application Layer Use Case @Injectable() export class GetSponsorsUseCase { constructor( private readonly sponsorRepository: ISponsorRepository, private readonly output: UseCaseOutputPort, ) {} async execute(): Promise> { const sponsors = await this.sponsorRepository.findAll() this.output.present({ sponsors }) return Result.ok(undefined) } } Rules: • execute() is the Input Port • The use case does not return result data • All output flows through the OutputPort • The return value signals success or failure only ### ⚠️ ARCHITECTURAL VIOLATION ALERT **The pattern shown above is INCORRECT and violates Clean Architecture.** #### ❌ WRONG PATTERN (What NOT to do) ```typescript @Injectable() export class GetSponsorsUseCase { constructor( private readonly sponsorRepository: ISponsorRepository, private readonly output: UseCaseOutputPort, ) {} async execute(): Promise> { const sponsors = await this.sponsorRepository.findAll() this.output.present({ sponsors }) // ❌ WRONG: Use case calling presenter return Result.ok(undefined) } } ``` **Why this violates Clean Architecture:** - Use cases **know about presenters** and how to call them - Creates **tight coupling** between application logic and presentation - Makes use cases **untestable** without mocking presenters - Violates the **Dependency Rule** (inner layer depending on outer layer behavior) #### ✅ CORRECT PATTERN (Clean Architecture) ```typescript @Injectable() export class GetSponsorsUseCase { constructor( private readonly sponsorRepository: ISponsorRepository, // NO output port needed in constructor ) {} async execute(): Promise> { const sponsors = await this.sponsorRepository.findAll() return Result.ok({ sponsors }) // ✅ Returns Result, period. No .present() call. } } ``` **The Controller (in API layer) handles the wiring:** ```typescript @Controller('/sponsors') export class SponsorsController { constructor( private readonly useCase: GetSponsorsUseCase, private readonly presenter: GetSponsorsPresenter, ) {} @Get() async getSponsors() { // 1. Execute use case const result = await this.useCase.execute() if (result.isErr()) { throw mapApplicationError(result.unwrapErr()) } // 2. Wire to presenter this.presenter.present(result.value) // 3. Return ViewModel return this.presenter.getViewModel() } } ``` **This is the ONLY pattern that respects Clean Architecture.** ~ Result Model type GetSponsorsResult = { sponsors: Sponsor[] } Rules: • Domain objects are allowed • No DTOs • No interfaces • No transport concerns ~ 3. API Layer API Services / Controllers (Thin Orchestration) The API layer is a transport boundary. It MUST delegate business logic to `./core`: • orchestrate auth + authorization checks (actor/session/roles) • collect/validate transport input (DTOs at the boundary) • execute a Core use case (entities/value objects live here) • map Result → DTO / ViewModel via a Presenter (presenter owns mapping) Rules: • Controllers stay thin: no business rules, no domain validation, no decision-making • API services orchestrate: auth + use case execution + presenter mapping • Domain objects never cross the API boundary un-mapped Presenter @Injectable() export class GetSponsorsPresenter implements UseCaseOutputPort { private viewModel!: GetSponsorsViewModel present(result: GetSponsorsResult): void { this.viewModel = { sponsors: result.sponsors.map(s => ({ id: s.id.value, name: s.name, websiteUrl: s.websiteUrl, })), } } getViewModel(): GetSponsorsViewModel { return this.viewModel } } ~ Controller @Controller('/sponsors') export class SponsorsController { constructor( private readonly useCase: GetSponsorsUseCase, private readonly presenter: GetSponsorsPresenter, ) {} @Get() async getSponsors() { const result = await this.useCase.execute() if (result.isErr()) { throw mapApplicationError(result.unwrapErr()) } return this.presenter.getViewModel() } } ~ Payments Example Application Layer Use Case @Injectable() export class CreatePaymentUseCase { constructor( private readonly paymentRepository: IPaymentRepository, private readonly output: UseCaseOutputPort, ) {} async execute(input: CreatePaymentInput): Promise>> { // business logic const payment = await this.paymentRepository.create(payment); this.output.present({ payment }); return Result.ok(undefined); } } Result Model type CreatePaymentResult = { payment: Payment; }; API Layer Presenter @Injectable() export class CreatePaymentPresenter implements UseCaseOutputPort { private viewModel: CreatePaymentViewModel | null = null; present(result: CreatePaymentResult): void { this.viewModel = { payment: this.mapPaymentToDto(result.payment), }; } getViewModel(): CreatePaymentViewModel | null { return this.viewModel; } reset(): void { this.viewModel = null; } private mapPaymentToDto(payment: Payment): PaymentDto { return { id: payment.id, // ... other fields }; } } Controller @Controller('/payments') export class PaymentsController { constructor( private readonly useCase: CreatePaymentUseCase, private readonly presenter: CreatePaymentPresenter, ) {} @Post() async createPayment(@Body() input: CreatePaymentInput) { const result = await this.useCase.execute(input); if (result.isErr()) { throw mapApplicationError(result.unwrapErr()); } return this.presenter.getViewModel(); } } ~ 4. Module Wiring (Composition Root) @Module({ providers: [ GetSponsorsUseCase, GetSponsorsPresenter, { provide: USE_CASE_OUTPUT_PORT, useExisting: GetSponsorsPresenter, }, ], }) export class SponsorsModule {} Rules: • The use case depends only on the OutputPort interface • The presenter is bound as the OutputPort implementation • process.env is not used inside the use case ~ 5. Explicitly Forbidden ❌ DTOs in use cases ❌ Domain objects returned directly to the API ❌ Output ports used as data structures ❌ present() returning a value ❌ Input data named InputPort ❌ Mapping logic inside use cases ❌ Environment access inside the core ~ Do / Don’t (Boundary Examples) ✅ DO: Keep pages/components consuming ViewModels returned by website services (DTOs stop at the service boundary), e.g. [LeagueAdminSchedulePage()](apps/website/app/leagues/[id]/schedule/admin/page.tsx:12). ✅ DO: Keep controllers/services thin and delegating, e.g. [LeagueController.createLeagueSeasonScheduleRace()](apps/api/src/domain/league/LeagueController.ts:291). ❌ DON’T: Put business rules in the API layer; rules belong in `./core` use cases/entities/value objects, e.g. [CreateLeagueSeasonScheduleRaceUseCase.execute()](core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase.ts:38). ~ 6. Optional Extensions Custom Output Ports Only introduce a dedicated OutputPort interface if: • multiple presentation paths exist • streaming or progress updates are required • more than one output method is needed interface ComplexOutputPort { presentSuccess(...) presentFailure(...) } ~ Input Port Interfaces Only introduce an explicit InputPort interface if: • multiple implementations of the same use case exist • feature flags or A/B variants are required • the use case itself must be substituted Otherwise: The use case class itself is the input port. ~ 7. Key Rules (Memorize These) Use cases answer what. Presenters answer how. Ports have behavior. Data does not. The core produces truth. The API interprets it. ~ TL;DR • Use cases are injected via DI • execute() is the Input Port • Outputs flow only through Output Ports • Results are business models, not DTOs • Interfaces exist only for behavior variability ### 🚨 CRITICAL CLEAN ARCHITECTURE CORRECTION **The examples in this document (sections 2, 3, and the Payments Example) demonstrate the WRONG pattern that violates Clean Architecture.** #### The Fundamental Problem The current architecture shows use cases **calling presenters directly**: ```typescript // ❌ WRONG - This violates Clean Architecture this.output.present({ sponsors }) ``` **This is architecturally incorrect.** Use cases must **never** know about presenters or call `.present()`. #### The Correct Clean Architecture Pattern **Use cases return Results. Controllers wire them to presenters.** ```typescript // ✅ CORRECT - Use case returns data @Injectable() export class GetSponsorsUseCase { constructor(private readonly sponsorRepository: ISponsorRepository) {} async execute(): Promise> { const sponsors = await this.sponsorRepository.findAll() return Result.ok({ sponsors }) // NO .present() call! } } // ✅ CORRECT - Controller handles wiring @Controller('/sponsors') export class SponsorsController { constructor( private readonly useCase: GetSponsorsUseCase, private readonly presenter: GetSponsorsPresenter, ) {} @Get() async getSponsors() { const result = await this.useCase.execute() if (result.isErr()) { throw mapApplicationError(result.unwrapErr()) } this.presenter.present(result.value) return this.presenter.getViewModel() } } ``` #### Why This Matters 1. **Dependency Rule**: Inner layers (use cases) cannot depend on outer layers (presenters) 2. **Testability**: Use cases can be tested without mocking presenters 3. **Flexibility**: Same use case can work with different presenters 4. **Separation of Concerns**: Use cases do business logic, presenters do transformation #### What Must Be Fixed **All use cases in the codebase must be updated to:** 1. **Remove** the `output: UseCaseOutputPort` constructor parameter 2. **Return** `Result` directly from `execute()` 3. **Remove** all `this.output.present()` calls **All controllers must be updated to:** 1. **Call** the use case and get the Result 2. **Pass** `result.value` to the presenter's `.present()` method 3. **Return** the presenter's `.getViewModel()` This is the **single source of truth** for correct Clean Architecture in this project.