docs
This commit is contained in:
@@ -1,468 +1,47 @@
|
||||
Clean Architecture – Application Services, Use Cases, Ports, and Data Flow (Strict, Final)
|
||||
# Clean Architecture Data Flow (Shared Contract)
|
||||
|
||||
This document defines the final, non-ambiguous Clean Architecture setup for the project.
|
||||
This document defines the **shared** data-flow rules that apply across all delivery applications.
|
||||
|
||||
It explicitly covers:
|
||||
• Use Cases vs Application Services
|
||||
• Input & Output Ports (and what does not exist)
|
||||
• API responsibilities
|
||||
• Frontend responsibilities
|
||||
• Naming, placement, and dependency rules
|
||||
• End-to-end flow with concrete paths and code examples
|
||||
It does not contain app-specific rules.
|
||||
|
||||
There are no hybrid concepts, no overloaded terms, and no optional interpretations.
|
||||
App-specific contracts:
|
||||
|
||||
⸻
|
||||
- Core: [`docs/architecture/core/CORE_DATA_FLOW.md`](docs/architecture/core/CORE_DATA_FLOW.md:1)
|
||||
- API: [`docs/architecture/api/API_DATA_FLOW.md`](docs/architecture/api/API_DATA_FLOW.md:1)
|
||||
- Website: [`docs/architecture/website/WEBSITE_DATA_FLOW.md`](docs/architecture/website/WEBSITE_DATA_FLOW.md:1)
|
||||
|
||||
1. Architectural Layers (Final)
|
||||
## 1) Dependency rule (non-negotiable)
|
||||
|
||||
Domain → Business truth
|
||||
Application → Use Cases + Application Services
|
||||
Adapters → API, Persistence, External Systems
|
||||
Frontend → UI, View Models, UX logic
|
||||
Dependencies point inward.
|
||||
|
||||
Only dependency-inward is allowed.
|
||||
```text
|
||||
Delivery apps → adapters → core
|
||||
```
|
||||
|
||||
⸻
|
||||
Core never depends on delivery apps.
|
||||
|
||||
2. Domain Layer (Core / Domain)
|
||||
## 2) Cross-boundary mapping rule
|
||||
|
||||
What lives here
|
||||
• Entities (classes)
|
||||
• Value Objects (classes)
|
||||
• Domain Services (stateless business logic)
|
||||
• Domain Events
|
||||
• Domain Errors / Invariants
|
||||
|
||||
What NEVER lives here
|
||||
• DTOs
|
||||
• Models
|
||||
• Ports
|
||||
• Use Cases
|
||||
• Application Services
|
||||
• Framework code
|
||||
|
||||
⸻
|
||||
|
||||
3. Application Layer (Core / Application)
|
||||
|
||||
The Application Layer has two distinct responsibilities:
|
||||
1. Use Cases – business decisions
|
||||
2. Application Services – orchestration of multiple use cases
|
||||
|
||||
⸻
|
||||
|
||||
4. Use Cases (Application / Use Cases)
|
||||
|
||||
Definition
|
||||
|
||||
A Use Case represents one business intent.
|
||||
If data crosses a boundary, it is mapped.
|
||||
|
||||
Examples:
|
||||
• CreateLeague
|
||||
• ApproveSponsorship
|
||||
• CompleteDriverOnboarding
|
||||
|
||||
Rules
|
||||
• A Use Case:
|
||||
• contains business logic
|
||||
• enforces invariants
|
||||
• operates on domain entities
|
||||
• communicates ONLY via ports
|
||||
• A Use Case:
|
||||
• does NOT orchestrate multiple workflows
|
||||
• does NOT know HTTP, UI, DB, queues
|
||||
- HTTP Request DTO is mapped to Core input.
|
||||
- Core result is mapped to HTTP Response DTO.
|
||||
|
||||
Structure
|
||||
## 3) Ownership rule
|
||||
|
||||
core/racing/application/use-cases/
|
||||
└─ CreateLeagueUseCase.ts
|
||||
Each layer owns its data shapes.
|
||||
|
||||
Example
|
||||
- Core owns domain and application models.
|
||||
- API owns HTTP DTOs.
|
||||
- Website owns ViewData and ViewModels.
|
||||
|
||||
export class CreateLeagueUseCase {
|
||||
constructor(
|
||||
private readonly leagueRepository: LeagueRepositoryPort,
|
||||
private readonly output: CreateLeagueOutputPort
|
||||
) {}
|
||||
No layer re-exports another layer’s models as-is across a boundary.
|
||||
|
||||
execute(input: CreateLeagueInputPort): void {
|
||||
// business rules & invariants
|
||||
## 4) Non-negotiable rules
|
||||
|
||||
const league = League.create(input.name, input.maxMembers);
|
||||
this.leagueRepository.save(league);
|
||||
1. Core contains business truth.
|
||||
2. Delivery apps translate and enforce.
|
||||
3. Adapters implement ports.
|
||||
|
||||
this.output.presentSuccess(league.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
5. Ports (Application / Ports)
|
||||
|
||||
The Only Two Kinds of Ports
|
||||
|
||||
Everything crossing the Application boundary is a Port.
|
||||
|
||||
Input Ports
|
||||
|
||||
Input Ports describe what a use case needs.
|
||||
|
||||
export interface CreateLeagueInputPort {
|
||||
readonly name: string;
|
||||
readonly maxMembers: number;
|
||||
}
|
||||
|
||||
Rules:
|
||||
• Interfaces only
|
||||
• No behavior
|
||||
• No validation logic
|
||||
|
||||
⸻
|
||||
|
||||
Output Ports
|
||||
|
||||
Output Ports describe how a use case emits outcomes.
|
||||
|
||||
export interface CreateLeagueOutputPort {
|
||||
presentSuccess(leagueId: string): void;
|
||||
presentFailure(reason: string): void;
|
||||
}
|
||||
|
||||
Rules:
|
||||
• No return values
|
||||
• No getters
|
||||
• No state
|
||||
• Use methods, not result objects
|
||||
|
||||
⸻
|
||||
|
||||
6. Application Services (Application / Services)
|
||||
|
||||
Definition
|
||||
|
||||
An Application Service orchestrates multiple Use Cases.
|
||||
|
||||
It exists because:
|
||||
• No single Use Case should know the whole workflow
|
||||
• Orchestration is not business logic
|
||||
|
||||
Rules
|
||||
• Application Services:
|
||||
• call multiple Use Cases
|
||||
• define execution order
|
||||
• handle partial failure & compensation
|
||||
• Application Services:
|
||||
• do NOT contain business rules
|
||||
• do NOT modify entities directly
|
||||
|
||||
Structure
|
||||
|
||||
core/racing/application/services/
|
||||
└─ LeagueSetupService.ts
|
||||
|
||||
Example (with Edge Cases)
|
||||
|
||||
export class LeagueSetupService {
|
||||
constructor(
|
||||
private readonly createLeague: CreateLeagueUseCase,
|
||||
private readonly createSeason: CreateSeasonUseCase,
|
||||
private readonly assignOwner: AssignLeagueOwnerUseCase,
|
||||
private readonly notify: SendLeagueWelcomeNotificationUseCase
|
||||
) {}
|
||||
|
||||
execute(input: LeagueSetupInputPort): void {
|
||||
const leagueId = this.createLeague.execute(input);
|
||||
|
||||
try {
|
||||
this.createSeason.execute({ leagueId });
|
||||
this.assignOwner.execute({ leagueId, ownerId: input.ownerId });
|
||||
this.notify.execute({ leagueId, ownerId: input.ownerId });
|
||||
} catch (error) {
|
||||
// compensation / rollback logic
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Edge cases that belong ONLY here:
|
||||
• Partial failure handling
|
||||
• Workflow order
|
||||
• Optional steps
|
||||
• Retry / idempotency logic
|
||||
|
||||
⸻
|
||||
|
||||
7. API Layer (apps/api)
|
||||
|
||||
Responsibilities
|
||||
• Transport (HTTP)
|
||||
• Validation (request shape)
|
||||
• Mapping to Input Ports
|
||||
• Calling Application Services
|
||||
• Adapting Output Ports
|
||||
|
||||
Structure
|
||||
|
||||
apps/api/leagues/
|
||||
├─ LeagueController.ts
|
||||
├─ presenters/
|
||||
│ └─ CreateLeaguePresenter.ts
|
||||
└─ dto/
|
||||
├─ CreateLeagueRequestDto.ts
|
||||
└─ CreateLeagueResponseDto.ts
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
API Presenter (Adapter)
|
||||
|
||||
export class CreateLeaguePresenter implements CreateLeagueOutputPort {
|
||||
private response!: CreateLeagueResponseDto;
|
||||
|
||||
presentSuccess(leagueId: string): void {
|
||||
this.response = { success: true, leagueId };
|
||||
}
|
||||
|
||||
presentFailure(reason: string): void {
|
||||
this.response = { success: false, errorMessage: reason };
|
||||
}
|
||||
|
||||
getResponse(): CreateLeagueResponseDto {
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
8. Frontend Layer (apps/website)
|
||||
|
||||
The frontend layer contains UI-specific data shapes. None of these cross into the Core.
|
||||
|
||||
Important: `apps/website` is a Next.js delivery app with SSR/RSC. This introduces one additional presentation concept to keep server/client boundaries correct.
|
||||
|
||||
There are four distinct frontend data concepts:
|
||||
1. API DTOs (transport)
|
||||
2. Command Models (user input / form state)
|
||||
3. View Models (client-only presentation classes)
|
||||
4. ViewData (template input, serializable)
|
||||
|
||||
⸻
|
||||
|
||||
8.1 API DTOs (Transport Contracts)
|
||||
|
||||
API DTOs represent exact HTTP contracts exposed by the backend.
|
||||
They are usually generated from OpenAPI or manually mirrored.
|
||||
|
||||
apps/website/lib/dtos/
|
||||
└─ CreateLeagueResponseDto.ts
|
||||
|
||||
Rules:
|
||||
• Exact mirror of backend response
|
||||
• No UI logic
|
||||
• No derived values
|
||||
• Never used directly by components
|
||||
|
||||
⸻
|
||||
|
||||
8.2 Command Models (User Input / Form State)
|
||||
|
||||
Command Models represent user intent before submission.
|
||||
They are frontend-only and exist to manage:
|
||||
• form state
|
||||
• validation feedback
|
||||
• step-based wizards
|
||||
|
||||
They are NOT:
|
||||
• domain objects
|
||||
• API DTOs
|
||||
• View Models
|
||||
|
||||
apps/website/lib/commands/
|
||||
└─ CreateLeagueCommandModel.ts
|
||||
|
||||
Rules:
|
||||
• Classes (stateful)
|
||||
• May contain client-side validation
|
||||
• May contain UX-specific helpers (step validation, dirty flags)
|
||||
• Must expose a method to convert to an API Request DTO
|
||||
|
||||
Example responsibility:
|
||||
• hold incomplete or invalid user input
|
||||
• guide the user through multi-step flows
|
||||
• prepare data for submission
|
||||
|
||||
Command Models:
|
||||
• are consumed by components
|
||||
• are passed into services
|
||||
• are never sent directly over HTTP
|
||||
|
||||
⸻
|
||||
|
||||
8.3 View Models (UI Display State)
|
||||
|
||||
View Models represent fully prepared UI state after data is loaded.
|
||||
|
||||
apps/website/lib/view-models/
|
||||
└─ CreateLeagueViewModel.ts
|
||||
|
||||
Rules:
|
||||
• Classes only
|
||||
• UI logic allowed (formatting, labels, derived flags)
|
||||
• No domain logic
|
||||
• No mutation after construction
|
||||
|
||||
SSR/RSC rule (website-only):
|
||||
• View Models are client-only and MUST NOT cross server-to-client boundaries.
|
||||
• Templates MUST NOT accept View Models.
|
||||
|
||||
⸻
|
||||
|
||||
8.4 Website Presenters (DTO → ViewModel)
|
||||
|
||||
Website Presenters are pure mappers.
|
||||
|
||||
export class CreateLeaguePresenter {
|
||||
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
|
||||
return new CreateLeagueViewModel(dto);
|
||||
}
|
||||
}
|
||||
|
||||
Rules:
|
||||
• Input: API DTOs
|
||||
• Output: View Models
|
||||
• No side effects
|
||||
• No API calls
|
||||
|
||||
⸻
|
||||
|
||||
8.5 Website Services (Orchestration)
|
||||
|
||||
Website Services orchestrate:
|
||||
• Command Models
|
||||
• API Client calls
|
||||
• Presenter mappings
|
||||
|
||||
export class LeagueService {
|
||||
async createLeague(command: CreateLeagueCommandModel): Promise<CreateLeagueViewModel> {
|
||||
const dto = await this.api.createLeague(command.toRequestDto());
|
||||
return this.presenter.present(dto);
|
||||
}
|
||||
}
|
||||
|
||||
Rules:
|
||||
• Services accept Command Models
|
||||
• Services return View Models
|
||||
• Components never call API clients directly
|
||||
|
||||
⸻
|
||||
|
||||
View Models (UI State)
|
||||
|
||||
apps/website/lib/view-models/
|
||||
└─ CreateLeagueViewModel.ts
|
||||
|
||||
export class CreateLeagueViewModel {
|
||||
constructor(private readonly dto: CreateLeagueResponseDto) {}
|
||||
|
||||
get message(): string {
|
||||
return this.dto.success
|
||||
? 'League created successfully'
|
||||
: this.dto.errorMessage ?? 'Creation failed';
|
||||
}
|
||||
}
|
||||
|
||||
Rules:
|
||||
• Classes only
|
||||
• UI logic allowed
|
||||
• No domain logic
|
||||
|
||||
⸻
|
||||
|
||||
Website Presenter (DTO → ViewModel)
|
||||
|
||||
export class CreateLeaguePresenter {
|
||||
present(dto: CreateLeagueResponseDto): CreateLeagueViewModel {
|
||||
return new CreateLeagueViewModel(dto);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
Website Service (Orchestration)
|
||||
|
||||
export class LeagueService {
|
||||
constructor(
|
||||
private readonly api: LeaguesApiClient,
|
||||
private readonly presenter: CreateLeaguePresenter
|
||||
) {}
|
||||
|
||||
async createLeague(input: unknown): Promise<CreateLeagueViewModel> {
|
||||
const dto = await this.api.createLeague(input);
|
||||
return this.presenter.present(dto);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
9. Full End-to-End Flow (Final)
|
||||
|
||||
UI Component
|
||||
→ Website Service
|
||||
→ API Client
|
||||
→ HTTP Request DTO
|
||||
→ API Controller
|
||||
→ Application Service
|
||||
→ Use Case(s)
|
||||
→ Domain
|
||||
→ Output Port
|
||||
→ API Presenter
|
||||
→ HTTP Response DTO
|
||||
→ Website Presenter
|
||||
→ View Model
|
||||
→ UI
|
||||
|
||||
|
||||
⸻
|
||||
|
||||
10. Final Non-Negotiable Rules
|
||||
• Core knows ONLY Ports + Domain
|
||||
• Core has NO Models, DTOs, or ViewModels
|
||||
• API talks ONLY to Application Services
|
||||
• Controllers NEVER call Use Cases directly
|
||||
• Frontend Components see ONLY ViewData (Templates) or ViewModels (Client orchestrators)
|
||||
• API DTOs never cross into Templates
|
||||
• View Models never cross into Templates
|
||||
|
||||
⸻
|
||||
|
||||
11. Final Merksatz
|
||||
|
||||
Use Cases decide.
|
||||
Application Services orchestrate.
|
||||
Adapters translate.
|
||||
UI presents.
|
||||
|
||||
If a class violates more than one of these roles, it is incorrectly placed.
|
||||
8.3.1 ViewData (Template Input)
|
||||
|
||||
ViewData is the only allowed input for Templates in `apps/website`.
|
||||
|
||||
Definition:
|
||||
• JSON-serializable data structure
|
||||
• Contains only primitives/arrays/plain objects
|
||||
• Ready to render: Templates perform no formatting and no derived computation
|
||||
|
||||
Rules:
|
||||
• ViewData is built in client code from:
|
||||
1) Page DTO (initial SSR-safe render)
|
||||
2) ViewModel (post-hydration enhancement)
|
||||
• ViewData MUST NOT contain ViewModel instances or Display Object instances.
|
||||
|
||||
Authoritative details:
|
||||
• [docs/architecture/website/VIEW_DATA.md](docs/architecture/website/VIEW_DATA.md:1)
|
||||
• [plans/nextjs-rsc-viewmodels-concept.md](plans/nextjs-rsc-viewmodels-concept.md:1)
|
||||
|
||||
Reference in New Issue
Block a user