|
1 | 1 | --- |
2 | | -description: Vibe coding guidelines and architectural constraints for Clean Architecture within the Architecture domain. |
3 | 2 | technology: Clean Architecture |
4 | 3 | domain: Architecture |
5 | 4 | level: Senior/Architect |
6 | 5 | version: Agnostic |
7 | 6 | tags: [architecture, system-design, clean-architecture, best-practices] |
8 | 7 | ai_role: Senior Architect |
9 | | -last_updated: 2026-03-22 |
10 | | -topic: Clean Architecture |
11 | | -complexity: Architect |
12 | | -last_evolution: 2026-03-29 |
13 | | -vibe_coding_ready: true--- |
14 | | - |
| 8 | +last_updated: 2026-03-29 |
| 9 | +--- |
15 | 10 |
|
16 | 11 | <div align="center"> |
17 | 12 | # 🏛️ Clean Architecture Production-Ready Best Practices |
18 | 13 | </div> |
19 | 14 | --- |
20 | 15 |
|
21 | 16 | Этот инженерный директив определяет **лучшие практики (best practices)** для архитектуры Clean Architecture. Данный документ спроектирован для обеспечения максимальной масштабируемости, безопасности и качества кода при разработке приложений корпоративного уровня. |
| 17 | + |
22 | 18 | # Context & Scope |
23 | 19 | - **Primary Goal:** Предоставить строгие архитектурные правила и практические паттерны для создания масштабируемых систем. |
24 | 20 | - **Description:** A concept created by Robert C. Martin (Uncle Bob). It separates a project into concentric rings. The main rule is the Dependency Rule: dependencies can only point inward. |
| 21 | + |
25 | 22 | ## Map of Patterns |
26 | 23 | - 📊 [**Data Flow:** Request and Event Lifecycle](./data-flow.md) |
27 | 24 | - 📁 [**Folder Structure:** Layering logic](./folder-structure.md) |
28 | 25 | - ⚖️ [**Trade-offs:** Pros, Cons, and System Constraints](./trade-offs.md) |
29 | 26 | - 🛠️ [**Implementation Guide:** Code patterns and Anti-patterns](./implementation-guide.md) |
| 27 | + |
30 | 28 | ## Core Principles |
31 | 29 |
|
32 | 30 | 1. **Isolation & Testability:** Changing a single feature doesn't break the entire business process. |
33 | 31 | 2. **Strict Boundaries:** Enforce rigid structural barriers between business logic and infrastructure. |
34 | 32 | 3. **Decoupling:** Decouple how data is stored from how it is queried and displayed. |
| 33 | + |
| 34 | +## Architecture Diagram |
| 35 | + |
| 36 | +```mermaid |
| 37 | +graph TD |
| 38 | + Infrastructure[Infrastructure] --> InterfaceAdapters[Interface Adapters] |
| 39 | + InterfaceAdapters --> UseCases[Use Cases] |
| 40 | + UseCases --> Domain[Domain Entities] |
| 41 | +
|
| 42 | + %% Added Design Token Styles for Mermaid Diagrams |
| 43 | + classDef default fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px,color:#000; |
| 44 | + classDef component fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000; |
| 45 | + classDef layout fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#000; |
| 46 | +
|
| 47 | + class Infrastructure component; |
| 48 | + class InterfaceAdapters component; |
| 49 | + class UseCases component; |
| 50 | + class Domain component; |
| 51 | +``` |
| 52 | + |
| 53 | +--- |
| 54 | + |
| 55 | +## 1. ORM Models Bleeding into Domain |
| 56 | + |
| 57 | +### ❌ Bad Practice |
| 58 | +```typescript |
| 59 | +// Domain Entity directly inherits from TypeORM BaseEntity |
| 60 | +import { BaseEntity, Column, Entity } from 'typeorm'; |
| 61 | + |
| 62 | +@Entity() |
| 63 | +export class User extends BaseEntity { |
| 64 | + @Column() |
| 65 | + email: string; |
| 66 | + |
| 67 | + public updateEmail(newEmail: string): void { |
| 68 | + this.email = newEmail; |
| 69 | + this.save(); // Hard infrastructure coupling |
| 70 | + } |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +### ⚠️ Problem |
| 75 | +The Domain layer (the core of the application) is tightly coupled with a specific third-party ORM library (`TypeORM`). This violates the Dependency Rule. Changing the database technology will require rewriting core business logic. |
| 76 | + |
| 77 | +### ✅ Best Practice |
| 78 | +```typescript |
| 79 | +// Pure Domain Entity completely agnostic of infrastructure |
| 80 | +export class User { |
| 81 | + constructor(private readonly id: string, private email: string) {} |
| 82 | + |
| 83 | + public updateEmail(newEmail: string): void { |
| 84 | + this.email = newEmail; |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +// Infrastructure Layer handles the ORM mapping |
| 89 | +@Entity('users') |
| 90 | +export class UserTypeOrmEntity { |
| 91 | + @PrimaryColumn() |
| 92 | + id: string; |
| 93 | + |
| 94 | + @Column() |
| 95 | + email: string; |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +### 🚀 Solution |
| 100 | +Isolate your Domain models from any external libraries. Use Data Mapper patterns in the Infrastructure layer to map between pure Domain entities and ORM-specific models. This ensures your core business logic is portable and testable without a database connection. |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +## 2. Direct Infrastructure Injection into Use Cases |
| 105 | + |
| 106 | +### ❌ Bad Practice |
| 107 | +```typescript |
| 108 | +import { S3Client } from 'aws-sdk'; |
| 109 | + |
| 110 | +export class UploadAvatarUseCase { |
| 111 | + constructor(private readonly s3Client: S3Client) {} |
| 112 | + |
| 113 | + public async execute(file: Buffer): Promise<string> { |
| 114 | + // Business logic depends strictly on AWS implementation |
| 115 | + const result = await this.s3Client.upload({ Bucket: 'av', Body: file }).promise(); |
| 116 | + return result.Location; |
| 117 | + } |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +### ⚠️ Problem |
| 122 | +The Use Case (Application layer) depends directly on an external hardware/infrastructure concern (`aws-sdk`). You cannot test this Use Case without mocking AWS S3, and you cannot switch to Azure or Google Cloud without modifying the Use Case. |
| 123 | + |
| 124 | +### ✅ Best Practice |
| 125 | +```typescript |
| 126 | +// Application Layer defines the abstraction (Port) |
| 127 | +export interface IFileStorageService { |
| 128 | + uploadFile(buffer: Buffer): Promise<string>; |
| 129 | +} |
| 130 | + |
| 131 | +// Application Layer Use Case depends on the abstraction |
| 132 | +export class UploadAvatarUseCase { |
| 133 | + constructor(private readonly storageService: IFileStorageService) {} |
| 134 | + |
| 135 | + public async execute(file: Buffer): Promise<string> { |
| 136 | + return this.storageService.uploadFile(file); |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +### 🚀 Solution |
| 142 | +Apply the Dependency Inversion Principle. The Application layer should define abstract interfaces (`Ports`) that dictate what it needs from the outside world. The Infrastructure layer implements these interfaces (`Adapters`). This guarantees true architectural decouple. |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +## 3. Fat Controllers Dictating Business Flow |
| 147 | + |
| 148 | +### ❌ Bad Practice |
| 149 | +```typescript |
| 150 | +class UserController { |
| 151 | + constructor(private readonly userRepository: IUserRepository) {} |
| 152 | + |
| 153 | + async registerUser(req: Request, res: Response) { |
| 154 | + const { email, password } = req.body; |
| 155 | + |
| 156 | + // Controller executing business validation and flow |
| 157 | + const existing = await this.userRepository.findByEmail(email); |
| 158 | + if (existing) { |
| 159 | + return res.status(400).send('Email taken'); |
| 160 | + } |
| 161 | + |
| 162 | + const user = new User(email, hash(password)); |
| 163 | + await this.userRepository.save(user); |
| 164 | + |
| 165 | + return res.status(201).send(user); |
| 166 | + } |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +### ⚠️ Problem |
| 171 | +The Controller (Interface Adapters layer) contains the business rules. It directly uses the Repository, bypassing the Application Use Case layer. This makes the logic difficult to reuse across different entry points (e.g., CLI, gRPC, HTTP). |
| 172 | + |
| 173 | +### ✅ Best Practice |
| 174 | +```typescript |
| 175 | +class UserController { |
| 176 | + constructor(private readonly registerUserUseCase: RegisterUserUseCase) {} |
| 177 | + |
| 178 | + async registerUser(req: Request, res: Response) { |
| 179 | + // Controller solely adapts HTTP into Use Case DTOs |
| 180 | + const result = await this.registerUserUseCase.execute({ |
| 181 | + email: req.body.email, |
| 182 | + password: req.body.password |
| 183 | + }); |
| 184 | + |
| 185 | + return res.status(201).send(result); |
| 186 | + } |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +### 🚀 Solution |
| 191 | +Controllers should be entirely "dumb". Their only responsibility is to parse incoming requests, pass standard DTOs to the corresponding Use Case, and format the output. All branching business logic and validation must reside inside the independent Use Case interactor. |
0 commit comments