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 ⸻ Result Model type GetSponsorsResult = { sponsors: Sponsor[] } Rules: • Domain objects are allowed • No DTOs • No interfaces • No transport concerns ⸻ 3. API Layer 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() } } ⸻ 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 ⸻ 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 This document is the single source of truth for use case architecture in this project.