refactor racing use cases

This commit is contained in:
2025-12-21 00:43:42 +01:00
parent e9d6f90bb2
commit c12656d671
308 changed files with 14401 additions and 7419 deletions
+256
View File
@@ -0,0 +1,256 @@
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<T> {
present(data: T): void
}
⸻
Presenter
• Implements UseCaseOutputPort<T>
• 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<GetSponsorsResult>,
) {}
async execute(): Promise<Result<void, ApplicationError>> {
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<GetSponsorsResult>
{
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.