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.