clean arch violations identified
This commit is contained in:
@@ -5,13 +5,13 @@ according to Clean Architecture, in a NestJS-based system.
|
||||
|
||||
The goal is:
|
||||
• strict separation of concerns
|
||||
• correct terminology (no fake “ports”)
|
||||
• 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)
|
||||
|
||||
@@ -24,7 +24,7 @@ Use Case
|
||||
|
||||
The public execute() method is the input port.
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Input
|
||||
• Pure data
|
||||
@@ -37,7 +37,7 @@ type GetSponsorsInput = {
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Result
|
||||
• The business outcome of a use case
|
||||
@@ -50,7 +50,7 @@ type GetSponsorsResult = {
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Output Port
|
||||
• A behavioral boundary
|
||||
@@ -63,7 +63,7 @@ export interface UseCaseOutputPort<T> {
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Presenter
|
||||
• Implements UseCaseOutputPort<T>
|
||||
@@ -72,7 +72,7 @@ Presenter
|
||||
• Holds internal state
|
||||
• Is pulled by the controller after execution
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
2. Canonical Use Case Structure
|
||||
|
||||
@@ -102,7 +102,85 @@ Rules:
|
||||
• All output flows through the OutputPort
|
||||
• The return value signals success or failure only
|
||||
|
||||
⸻
|
||||
### ⚠️ ARCHITECTURAL VIOLATION ALERT
|
||||
|
||||
**The pattern shown above is INCORRECT and violates Clean Architecture.**
|
||||
|
||||
#### ❌ WRONG PATTERN (What NOT to do)
|
||||
|
||||
```typescript
|
||||
@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 }) // ❌ WRONG: Use case calling presenter
|
||||
return Result.ok(undefined)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this violates Clean Architecture:**
|
||||
- Use cases **know about presenters** and how to call them
|
||||
- Creates **tight coupling** between application logic and presentation
|
||||
- Makes use cases **untestable** without mocking presenters
|
||||
- Violates the **Dependency Rule** (inner layer depending on outer layer behavior)
|
||||
|
||||
#### ✅ CORRECT PATTERN (Clean Architecture)
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class GetSponsorsUseCase {
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
// NO output port needed in constructor
|
||||
) {}
|
||||
|
||||
async execute(): Promise<Result<GetSponsorsResult, ApplicationError>> {
|
||||
const sponsors = await this.sponsorRepository.findAll()
|
||||
|
||||
return Result.ok({ sponsors })
|
||||
// ✅ Returns Result, period. No .present() call.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**The Controller (in API layer) handles the wiring:**
|
||||
|
||||
```typescript
|
||||
@Controller('/sponsors')
|
||||
export class SponsorsController {
|
||||
constructor(
|
||||
private readonly useCase: GetSponsorsUseCase,
|
||||
private readonly presenter: GetSponsorsPresenter,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
async getSponsors() {
|
||||
// 1. Execute use case
|
||||
const result = await this.useCase.execute()
|
||||
|
||||
if (result.isErr()) {
|
||||
throw mapApplicationError(result.unwrapErr())
|
||||
}
|
||||
|
||||
// 2. Wire to presenter
|
||||
this.presenter.present(result.value)
|
||||
|
||||
// 3. Return ViewModel
|
||||
return this.presenter.getViewModel()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**This is the ONLY pattern that respects Clean Architecture.**
|
||||
|
||||
~
|
||||
|
||||
Result Model
|
||||
|
||||
@@ -116,7 +194,7 @@ Rules:
|
||||
• No interfaces
|
||||
• No transport concerns
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
3. API Layer
|
||||
|
||||
@@ -158,7 +236,7 @@ export class GetSponsorsPresenter
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Controller
|
||||
|
||||
@@ -182,7 +260,7 @@ export class SponsorsController {
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Payments Example
|
||||
|
||||
@@ -266,7 +344,7 @@ export class PaymentsController {
|
||||
}
|
||||
}
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
4. Module Wiring (Composition Root)
|
||||
|
||||
@@ -287,7 +365,7 @@ Rules:
|
||||
• The presenter is bound as the OutputPort implementation
|
||||
• process.env is not used inside the use case
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
5. Explicitly Forbidden
|
||||
|
||||
@@ -299,7 +377,7 @@ Rules:
|
||||
❌ Mapping logic inside use cases
|
||||
❌ Environment access inside the core
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Do / Don’t (Boundary Examples)
|
||||
|
||||
@@ -307,6 +385,8 @@ Do / Don’t (Boundary Examples)
|
||||
✅ DO: Keep controllers/services thin and delegating, e.g. [LeagueController.createLeagueSeasonScheduleRace()](apps/api/src/domain/league/LeagueController.ts:291).
|
||||
❌ DON’T: Put business rules in the API layer; rules belong in `./core` use cases/entities/value objects, e.g. [CreateLeagueSeasonScheduleRaceUseCase.execute()](core/racing/application/use-cases/CreateLeagueSeasonScheduleRaceUseCase.ts:38).
|
||||
|
||||
~
|
||||
|
||||
6. Optional Extensions
|
||||
|
||||
Custom Output Ports
|
||||
@@ -322,7 +402,7 @@ interface ComplexOutputPort {
|
||||
}
|
||||
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
Input Port Interfaces
|
||||
|
||||
@@ -335,7 +415,7 @@ Otherwise:
|
||||
|
||||
The use case class itself is the input port.
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
7. Key Rules (Memorize These)
|
||||
|
||||
@@ -348,7 +428,7 @@ Data does not.
|
||||
The core produces truth.
|
||||
The API interprets it.
|
||||
|
||||
⸻
|
||||
~
|
||||
|
||||
TL;DR
|
||||
• Use cases are injected via DI
|
||||
@@ -357,4 +437,77 @@ TL;DR
|
||||
• 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.
|
||||
### 🚨 CRITICAL CLEAN ARCHITECTURE CORRECTION
|
||||
|
||||
**The examples in this document (sections 2, 3, and the Payments Example) demonstrate the WRONG pattern that violates Clean Architecture.**
|
||||
|
||||
#### The Fundamental Problem
|
||||
|
||||
The current architecture shows use cases **calling presenters directly**:
|
||||
|
||||
```typescript
|
||||
// ❌ WRONG - This violates Clean Architecture
|
||||
this.output.present({ sponsors })
|
||||
```
|
||||
|
||||
**This is architecturally incorrect.** Use cases must **never** know about presenters or call `.present()`.
|
||||
|
||||
#### The Correct Clean Architecture Pattern
|
||||
|
||||
**Use cases return Results. Controllers wire them to presenters.**
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use case returns data
|
||||
@Injectable()
|
||||
export class GetSponsorsUseCase {
|
||||
constructor(private readonly sponsorRepository: ISponsorRepository) {}
|
||||
|
||||
async execute(): Promise<Result<GetSponsorsResult, ApplicationError>> {
|
||||
const sponsors = await this.sponsorRepository.findAll()
|
||||
return Result.ok({ sponsors })
|
||||
// NO .present() call!
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CORRECT - Controller handles wiring
|
||||
@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())
|
||||
}
|
||||
|
||||
this.presenter.present(result.value)
|
||||
return this.presenter.getViewModel()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Why This Matters
|
||||
|
||||
1. **Dependency Rule**: Inner layers (use cases) cannot depend on outer layers (presenters)
|
||||
2. **Testability**: Use cases can be tested without mocking presenters
|
||||
3. **Flexibility**: Same use case can work with different presenters
|
||||
4. **Separation of Concerns**: Use cases do business logic, presenters do transformation
|
||||
|
||||
#### What Must Be Fixed
|
||||
|
||||
**All use cases in the codebase must be updated to:**
|
||||
1. **Remove** the `output: UseCaseOutputPort<T>` constructor parameter
|
||||
2. **Return** `Result<T, E>` directly from `execute()`
|
||||
3. **Remove** all `this.output.present()` calls
|
||||
|
||||
**All controllers must be updated to:**
|
||||
1. **Call** the use case and get the Result
|
||||
2. **Pass** `result.value` to the presenter's `.present()` method
|
||||
3. **Return** the presenter's `.getViewModel()`
|
||||
|
||||
This is the **single source of truth** for correct Clean Architecture in this project.
|
||||
Reference in New Issue
Block a user