refactor dtos to ports

This commit is contained in:
2025-12-19 15:07:53 +01:00
parent 499562c456
commit 8116fe888f
46 changed files with 718 additions and 266 deletions

339
docs/architecture/ENUMS.md Normal file
View File

@@ -0,0 +1,339 @@
Enums in Clean Architecture (Strict & Final)
This document defines how enums are modeled, placed, and used in a strict Clean Architecture setup.
Enums are one of the most common sources of architectural leakage. This guide removes all ambiguity.
1. Core Principle
Enums represent knowledge.
Knowledge must live where it is true.
Therefore:
• Not every enum is a domain enum
• Enums must never cross architectural boundaries blindly
• Ports must remain neutral
2. Enum Categories (Authoritative)
There are four and only four valid enum categories:
1. Domain Enums
2. Application (Workflow) Enums
3. Transport Enums (API)
4. UI Enums (Frontend)
Each category has strict placement and usage rules.
3. Domain Enums
Definition
A Domain Enum represents a business concept that:
• has meaning in the domain
• affects rules or invariants
• is part of the ubiquitous language
Examples:
• LeagueVisibility
• MembershipRole
• RaceStatus
• SponsorshipTier
• PenaltyType
Placement
core/<context>/domain/
├── value-objects/
│ └── LeagueVisibility.ts
└── entities/
Preferred: model domain enums as Value Objects instead of enum keywords.
Example (Value Object)
export class LeagueVisibility {
private constructor(private readonly value: 'public' | 'private') {}
static from(value: string): LeagueVisibility {
if (value !== 'public' && value !== 'private') {
throw new DomainError('Invalid LeagueVisibility');
}
return new LeagueVisibility(value);
}
isPublic(): boolean {
return this.value === 'public';
}
}
Usage Rules
Allowed:
• Domain
• Use Cases
Forbidden:
• Ports
• Adapters
• API DTOs
• Frontend
Domain enums must never cross a Port boundary.
4. Application Enums (Workflow Enums)
Definition
Application Enums represent internal workflow or state coordination.
They are not business truth and must not leak.
Examples:
• LeagueSetupStep
• ImportPhase
• ProcessingState
Placement
core/<context>/application/internal/
└── LeagueSetupStep.ts
Example
export enum LeagueSetupStep {
CreateLeague,
CreateSeason,
AssignOwner,
Notify
}
Usage Rules
Allowed:
• Application Services
• Use Cases
Forbidden:
• Domain
• Ports
• Adapters
• Frontend
These enums must remain strictly internal.
5. Transport Enums (API DTOs)
Definition
Transport Enums describe allowed values in HTTP contracts.
They exist purely to constrain transport data, not to encode business rules.
Naming rule:
Transport enums MUST end with Enum.
This makes enums immediately recognizable in code reviews and prevents silent leakage.
Examples:
• LeagueVisibilityEnum
• SponsorshipStatusEnum
• PenaltyTypeEnum
Placement
apps/api/<feature>/dto/
└── LeagueVisibilityEnum.ts
Website mirrors the same naming:
apps/website/lib/dtos/
└── LeagueVisibilityEnum.ts
Example
export enum LeagueVisibilityEnum {
Public = 'public',
Private = 'private'
}
Usage Rules
Allowed:
• API Controllers
• API Presenters
• Website API DTOs
Forbidden:
• Core Domain
• Use Cases
Transport enums are copies, never reexports of domain enums.
Placement
apps/api/<feature>/dto/
└── LeagueVisibilityDto.ts
or inline as union types in DTOs.
Example
export type LeagueVisibilityDto = 'public' | 'private';
Usage Rules
Allowed:
• API Controllers
• API Presenters
• Website API DTOs
Forbidden:
• Core Domain
• Use Cases
Transport enums are copies, never reexports of domain enums.
6. UI Enums (Frontend)
Definition
UI Enums describe presentation or interaction state.
They have no business meaning.
Examples:
• WizardStep
• SortOrder
• ViewMode
• TabKey
Placement
apps/website/lib/ui/
└── LeagueWizardStep.ts
Example
export enum LeagueWizardStep {
Basics,
Structure,
Scoring,
Review
}
Usage Rules
Allowed:
• Frontend only
Forbidden:
• Core
• API
7. Absolute Prohibitions
❌ Enums in Ports
// ❌ forbidden
export interface CreateLeagueInputPort {
visibility: LeagueVisibility;
}
✅ Correct
export interface CreateLeagueInputPort {
visibility: 'public' | 'private';
}
Mapping happens inside the Use Case:
const visibility = LeagueVisibility.from(input.visibility);
8. Decision Checklist
Ask these questions:
1. Does changing this enum change business rules?
• Yes → Domain Enum
• No → continue
2. Is it only needed for internal workflow coordination?
• Yes → Application Enum
• No → continue
3. Is it part of an HTTP contract?
• Yes → Transport Enum
• No → continue
4. Is it purely for UI state?
• Yes → UI Enum
9. Summary Table
Enum Type Location May Cross Ports Scope
Domain Enum core/domain ❌ No Business rules
Application Enum core/application ❌ No Workflow only
Transport Enum apps/api + website ❌ No HTTP contracts
UI Enum apps/website ❌ No Presentation only
10. Final Rule (Non-Negotiable)
If an enum crosses a boundary, it is in the wrong place.
This rule alone prevents most long-term architectural decay.