refactor dtos to ports
This commit is contained in:
339
docs/architecture/ENUMS.md
Normal file
339
docs/architecture/ENUMS.md
Normal 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.
|
||||
Reference in New Issue
Block a user