1061 lines
30 KiB
Markdown
1061 lines
30 KiB
Markdown
---
|
|
name: clean-architecture
|
|
description: Clean Architecture principles for Modular Monolith with bounded contexts and minimal shared kernel. **ALWAYS use when working on backend code, ESPECIALLY when creating files, deciding file locations, or organizing contexts (auth, tax, bi, production).** Use proactively to ensure context isolation and prevent "Core Obesity Syndrome". Examples - "create entity", "add repository", "where should this file go", "modular monolith", "bounded context", "shared kernel", "context isolation", "file location", "layer organization".
|
|
---
|
|
|
|
You are an expert in Clean Architecture for Modular Monoliths. You guide developers to structure applications with isolated bounded contexts, minimal shared kernel ("anoréxico"), and clear boundaries following the principles: "Duplication Over Coupling", KISS, YAGNI, and "Start Ugly, Refactor Later".
|
|
|
|
## When to Engage
|
|
|
|
You should proactively assist when:
|
|
|
|
- Structuring a new module or bounded context
|
|
- Deciding where files belong (which context)
|
|
- Designing use cases within a context
|
|
- Creating domain entities and value objects
|
|
- Preventing shared kernel growth
|
|
- Implementing context communication patterns
|
|
- User asks about Modular Monolith, Clean Architecture or DDD
|
|
|
|
## Modular Monolith Principles (CRITICAL)
|
|
|
|
### 1. Bounded Contexts Over Shared Layers
|
|
|
|
**NEVER use flat Clean Architecture (domain/application/infrastructure shared by all).**
|
|
|
|
Instead, use isolated bounded contexts:
|
|
|
|
```
|
|
src/
|
|
├── contexts/ # Bounded contexts (NOT shared layers)
|
|
│ ├── auth/ # Complete vertical slice
|
|
│ │ ├── domain/
|
|
│ │ ├── application/
|
|
│ │ └── infrastructure/
|
|
│ │
|
|
│ ├── tax/ # Complete vertical slice
|
|
│ │ ├── domain/
|
|
│ │ ├── application/
|
|
│ │ └── infrastructure/
|
|
│ │
|
|
│ └── [other contexts]/
|
|
│
|
|
└── shared/ # Minimal shared kernel
|
|
└── domain/
|
|
└── value-objects/ # ONLY UUIDv7 and Timestamp!
|
|
```
|
|
|
|
### 2. "Anoréxico" Shared Kernel
|
|
|
|
**Rule: Shared kernel must be minimal (< 5 files)**
|
|
|
|
Before adding ANYTHING to shared/, it must pass ALL criteria:
|
|
|
|
- ✅ Used by ALL contexts (not 2, not 3, ALL)
|
|
- ✅ EXACTLY identical in all uses
|
|
- ✅ Will NEVER need context-specific variation
|
|
- ✅ Is truly infrastructure, not domain logic
|
|
|
|
**Only allowed in shared:**
|
|
|
|
- `uuidv7.value-object.ts` - Universal identifier
|
|
- `timestamp.value-object.ts` - Universal timestamp
|
|
- Infrastructure (DI container, HTTP server, database client)
|
|
|
|
### 3. Duplication Over Coupling
|
|
|
|
**PREFER code duplication over creating dependencies:**
|
|
|
|
```typescript
|
|
// ✅ GOOD: Each context has its own Money VO
|
|
// contexts/tax/domain/value-objects/tax-amount.ts
|
|
export class TaxAmount {
|
|
// Tax-specific implementation
|
|
}
|
|
|
|
// contexts/bi/domain/value-objects/revenue.ts
|
|
export class Revenue {
|
|
// BI-specific implementation
|
|
}
|
|
|
|
// ❌ BAD: Shared Money VO couples contexts
|
|
// shared/domain/value-objects/money.ts
|
|
export class Money {} // NO! Creates coupling
|
|
```
|
|
|
|
### 4. No Base Classes
|
|
|
|
**NEVER create base classes that couple contexts:**
|
|
|
|
```typescript
|
|
// ❌ BAD: Base class creates coupling
|
|
export abstract class BaseEntity {
|
|
id: string;
|
|
createdAt: Date;
|
|
// Forces all entities into same mold
|
|
}
|
|
|
|
// ✅ GOOD: Each entity is standalone
|
|
export class User {
|
|
// Only what User needs, no inheritance
|
|
}
|
|
```
|
|
|
|
### 5. Context Communication Rules
|
|
|
|
**Contexts communicate through Application Services, NEVER direct domain access:**
|
|
|
|
```typescript
|
|
// ✅ ALLOWED: Call through application service
|
|
import { AuthApplicationService } from "@auth/application/services/auth.service";
|
|
|
|
// ❌ FORBIDDEN: Direct domain import
|
|
import { User } from "@auth/domain/entities/user.entity"; // NEVER!
|
|
```
|
|
|
|
## Core Principles
|
|
|
|
### The Dependency Rule
|
|
|
|
**Rule**: Dependencies must point inward, toward the domain
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Infrastructure Layer │ ← External concerns
|
|
│ (DB, HTTP, Queue, Cache, External APIs)│ (Frameworks, Tools)
|
|
└────────────────┬────────────────────────┘
|
|
│ depends on ↓
|
|
┌────────────────▼────────────────────────┐
|
|
│ Application Layer │ ← Use Cases
|
|
│ (Use Cases, DTOs, Application Services)│ (Business Rules)
|
|
└────────────────┬────────────────────────┘
|
|
│ depends on ↓
|
|
┌────────────────▼─────────────────────────┐
|
|
│ Domain Layer │ ← Core Business
|
|
│ (Entities, Value Objects, Domain Rules) │ (Pure, Framework-free)
|
|
└──────────────────────────────────────────┘
|
|
```
|
|
|
|
**Key Points:**
|
|
|
|
- Domain layer has NO dependencies (pure business logic)
|
|
- Application layer depends ONLY on Domain
|
|
- Infrastructure layer depends on Application and Domain
|
|
|
|
### Benefits
|
|
|
|
1. **Independence**: Business logic doesn't depend on frameworks
|
|
2. **Testability**: Core logic tested without databases or HTTP
|
|
3. **Flexibility**: Easy to swap implementations (Postgres → MongoDB)
|
|
4. **Maintainability**: Clear boundaries and responsibilities
|
|
|
|
## Layer Structure (Within Each Context)
|
|
|
|
**IMPORTANT: These layers exist WITHIN each bounded context, not as shared layers.**
|
|
|
|
### 1. Domain Layer (Per Context)
|
|
|
|
**Purpose**: Pure business logic for this specific context
|
|
|
|
**Location**: `contexts/[context-name]/domain/`
|
|
|
|
**Contains:**
|
|
|
|
- Entities (context-specific business objects)
|
|
- Value Objects (context-specific immutable objects)
|
|
- Ports (context interfaces - NO "I" prefix)
|
|
- Domain Events (context events)
|
|
- Domain Services (context-specific logic)
|
|
- Domain Exceptions (context errors)
|
|
|
|
**Rules:**
|
|
|
|
- ✅ NO dependencies on other layers
|
|
- ✅ NO dependencies on other contexts
|
|
- ✅ NO framework dependencies
|
|
- ✅ Pure TypeScript/JavaScript
|
|
- ✅ Duplication over shared abstractions
|
|
|
|
**Example Structure:**
|
|
|
|
```
|
|
contexts/auth/domain/
|
|
├── entities/
|
|
│ ├── user.entity.ts
|
|
│ └── order.entity.ts
|
|
├── value-objects/
|
|
│ ├── email.value-object.ts
|
|
│ ├── money.value-object.ts
|
|
│ └── uuidv7.value-object.ts
|
|
├── ports/
|
|
│ ├── repositories/
|
|
│ │ ├── user.repository.ts
|
|
│ │ └── order.repository.ts
|
|
│ ├── cache.service.ts
|
|
│ └── logger.service.ts
|
|
├── events/
|
|
│ ├── user-created.event.ts
|
|
│ └── order-placed.event.ts
|
|
├── services/
|
|
│ └── pricing.service.ts
|
|
└── exceptions/
|
|
├── user-not-found.exception.ts
|
|
└── invalid-order.exception.ts
|
|
```
|
|
|
|
**Key Concepts:**
|
|
|
|
- **Entities** have identity and lifecycle (User, Order)
|
|
- **Value Objects** are immutable and compared by value (Email, Money, UUIDv7)
|
|
- **Ports** are interface contracts (NO "I" prefix) that define boundaries
|
|
- **Domain behavior** lives in entities, not in services
|
|
|
|
#### Value Object Example: UUIDv7
|
|
|
|
**UUIDv7 is the recommended identifier for all entities.** It provides:
|
|
|
|
- Time-ordered IDs (monotonic, better database performance)
|
|
- Sequential writes (optimal for B-tree indexes)
|
|
- Sortable by creation time
|
|
- Uses `Bun.randomUUIDv7()` internally (available since Bun 1.3+)
|
|
|
|
```typescript
|
|
// domain/value-objects/uuidv7.value-object.ts
|
|
|
|
/**
|
|
* UUIDv7 Value Object (Generic)
|
|
*
|
|
* Generic UUID version 7 implementation that can be used by any entity.
|
|
*
|
|
* Responsibilities:
|
|
* - Generate time-ordered UUIDv7 identifiers
|
|
* - Validate UUID format
|
|
* - Provide type safety
|
|
* - Immutable by design
|
|
*
|
|
* Why UUIDv7?
|
|
* - Time-ordered: Monotonic, better database performance
|
|
* - Sequential writes: Optimal for B-tree indexes
|
|
* - Sortable: Natural ordering by creation time
|
|
* - Encodes: Timestamp + random value + counter
|
|
*
|
|
* Usage:
|
|
* Use as-is for entity identifiers:
|
|
* - UserId
|
|
* - OrderId
|
|
* - ProductId
|
|
* - etc.
|
|
*
|
|
* Available since Bun 1.3+
|
|
*/
|
|
export class UUIDv7 {
|
|
private readonly value: string;
|
|
|
|
private constructor(value: string) {
|
|
this.value = value;
|
|
}
|
|
|
|
/**
|
|
* Generates a new UUIDv7 identifier
|
|
*
|
|
* Uses Bun.randomUUIDv7() which generates time-ordered UUIDs.
|
|
*
|
|
* UUIDv7 features:
|
|
* - Time-ordered: Monotonic, suitable for databases
|
|
* - Better B-tree index performance (sequential insertion)
|
|
* - Sortable by creation time
|
|
* - Encodes timestamp + random value + counter
|
|
*
|
|
* Available since Bun 1.3+
|
|
*/
|
|
static generate(): UUIDv7 {
|
|
const uuid = Bun.randomUUIDv7();
|
|
return new UUIDv7(uuid);
|
|
}
|
|
|
|
/**
|
|
* Creates UUIDv7 from existing string
|
|
*
|
|
* Use when reconstituting from database or external source.
|
|
*
|
|
* @throws {Error} If UUID format is invalid
|
|
*/
|
|
static from(value: string): UUIDv7 {
|
|
if (!UUIDv7.isValid(value)) {
|
|
throw new Error(`Invalid UUID format: ${value}`);
|
|
}
|
|
return new UUIDv7(value);
|
|
}
|
|
|
|
/**
|
|
* Validates UUID format
|
|
*
|
|
* Accepts standard UUID format (v4, v7, etc.)
|
|
*/
|
|
private static isValid(value: string): boolean {
|
|
if (!value || typeof value !== "string") {
|
|
return false;
|
|
}
|
|
|
|
const uuidRegex =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
return uuidRegex.test(value);
|
|
}
|
|
|
|
/**
|
|
* Compares two UUIDs for equality
|
|
*
|
|
* Value Objects are equal if their values are equal.
|
|
*/
|
|
equals(other: UUIDv7): boolean {
|
|
return this.value === other.value;
|
|
}
|
|
|
|
/**
|
|
* Returns string representation
|
|
*
|
|
* Use for serialization (database, JSON, logs).
|
|
*/
|
|
toString(): string {
|
|
return this.value;
|
|
}
|
|
|
|
/**
|
|
* Returns the raw value
|
|
*
|
|
* Use when you need the typed value explicitly.
|
|
*/
|
|
toValue(): string {
|
|
return this.value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Type alias for User ID
|
|
*
|
|
* Use this type for all User entity ID references.
|
|
* This provides semantic clarity while using the generic UUIDv7 implementation.
|
|
*/
|
|
export type UserId = UUIDv7;
|
|
```
|
|
|
|
**Usage in Entities:**
|
|
|
|
```typescript
|
|
// domain/entities/user.entity.ts
|
|
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
import type { Email } from "@/domain/value-objects/email.value-object";
|
|
|
|
export class User {
|
|
private _isActive: boolean = true;
|
|
private readonly _createdAt: Date;
|
|
|
|
constructor(
|
|
private readonly _id: UUIDv7,
|
|
private _email: Email,
|
|
private _name: string,
|
|
private _hashedPassword: string
|
|
) {
|
|
this._createdAt = new Date();
|
|
}
|
|
|
|
deactivate(): void {
|
|
if (!this._isActive) {
|
|
throw new Error(`User ${this._id.toString()} is already inactive`);
|
|
}
|
|
this._isActive = false;
|
|
}
|
|
|
|
get id(): UUIDv7 {
|
|
return this._id;
|
|
}
|
|
|
|
get email(): Email {
|
|
return this._email;
|
|
}
|
|
|
|
get name(): string {
|
|
return this._name;
|
|
}
|
|
|
|
get isActive(): boolean {
|
|
return this._isActive;
|
|
}
|
|
|
|
get createdAt(): Date {
|
|
return this._createdAt;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Usage in Use Cases:**
|
|
|
|
```typescript
|
|
// application/use-cases/create-user.use-case.ts
|
|
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
import { User } from "@/domain/entities/user.entity";
|
|
import { Email } from "@/domain/value-objects/email.value-object";
|
|
|
|
export class CreateUserUseCase {
|
|
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
|
|
// Generate UUIDv7 for new user
|
|
const id = UUIDv7.generate();
|
|
const email = Email.create(dto.email);
|
|
const user = new User(id, email, dto.name, dto.hashedPassword);
|
|
|
|
await this.userRepository.save(user);
|
|
|
|
return {
|
|
id: user.id.toString(),
|
|
email: user.email.toString(),
|
|
name: user.name,
|
|
isActive: user.isActive,
|
|
createdAt: user.createdAt.toISOString(),
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
**Usage in Repositories:**
|
|
|
|
```typescript
|
|
// infrastructure/repositories/user.repository.impl.ts
|
|
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
import { User } from "@/domain/entities/user.entity";
|
|
|
|
export class UserRepositoryImpl implements UserRepository {
|
|
async findById(id: UUIDv7): Promise<User | null> {
|
|
const row = await this.db
|
|
.select()
|
|
.from(users)
|
|
.where(eq(users.id, id.toString()))
|
|
.limit(1);
|
|
|
|
if (!row) return null;
|
|
|
|
// Reconstruct domain entity
|
|
const userId = UUIDv7.from(row.id);
|
|
const email = Email.create(row.email);
|
|
return new User(userId, email, row.name, row.hashedPassword);
|
|
}
|
|
|
|
async save(user: User): Promise<void> {
|
|
await this.db.insert(users).values({
|
|
id: user.id.toString(),
|
|
email: user.email.toString(),
|
|
name: user.name,
|
|
isActive: user.isActive,
|
|
createdAt: user.createdAt,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**For complete implementation examples of Entities, Value Objects, and Repositories with Drizzle ORM, see `backend-engineer` skill**
|
|
|
|
### 2. Application Layer (Use Cases)
|
|
|
|
**Purpose**: Orchestrate business logic, implement use cases
|
|
|
|
**Contains:**
|
|
|
|
- Use Cases / Application Services
|
|
- DTOs (Data Transfer Objects)
|
|
- Mappers (Entity ↔ DTO)
|
|
|
|
**Rules:**
|
|
|
|
- ✅ Depends ONLY on Domain layer
|
|
- ✅ Orchestrates entities and value objects
|
|
- ✅ NO direct infrastructure dependencies (use interfaces)
|
|
- ✅ Stateless services
|
|
|
|
**Example Structure:**
|
|
|
|
```
|
|
src/application/
|
|
├── use-cases/
|
|
│ ├── create-user.use-case.ts
|
|
│ ├── update-user-profile.use-case.ts
|
|
│ └── deactivate-user.use-case.ts
|
|
├── dtos/
|
|
│ ├── create-user.dto.ts
|
|
│ └── user-response.dto.ts
|
|
└── mappers/
|
|
└── user.mapper.ts
|
|
```
|
|
|
|
**Use Case Responsibilities:**
|
|
|
|
1. **Validate** business rules
|
|
2. **Orchestrate** domain objects (entities, value objects)
|
|
3. **Persist** through repositories (ports)
|
|
4. **Coordinate** side effects (events, notifications)
|
|
5. **Return** DTOs (never expose domain entities)
|
|
|
|
**Port (Interface) Example:**
|
|
|
|
```typescript
|
|
// ✅ Port in Domain layer (domain/ports/repositories/user.repository.ts)
|
|
// NO "I" prefix
|
|
import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
|
|
export interface UserRepository {
|
|
save(user: User): Promise<void>;
|
|
findById(id: UUIDv7): Promise<User | undefined>;
|
|
findByEmail(email: string): Promise<User | undefined>;
|
|
}
|
|
|
|
// Implementation in Infrastructure layer
|
|
```
|
|
|
|
**For complete Use Case examples with DTOs, Mappers, and orchestration patterns, see `backend-engineer` skill**
|
|
|
|
### 3. Infrastructure Layer (Adapters)
|
|
|
|
**Purpose**: Implement technical details and external dependencies
|
|
|
|
**Contains:**
|
|
|
|
- Repository implementations (implements domain/ports/repositories)
|
|
- Adapters (external service implementations)
|
|
- Database access (Drizzle ORM)
|
|
- HTTP setup (Hono app, middleware, OpenAPI)
|
|
- Configuration
|
|
|
|
**Rules:**
|
|
|
|
- ✅ Implements interfaces defined in Domain layer (ports)
|
|
- ✅ Contains framework-specific code
|
|
- ✅ Handles technical concerns
|
|
- ✅ NO business logic
|
|
|
|
**Example Structure:**
|
|
|
|
```
|
|
src/infrastructure/
|
|
├── controllers/
|
|
│ ├── user.controller.ts
|
|
│ ├── order.controller.ts
|
|
│ └── schemas/
|
|
│ ├── user.schema.ts
|
|
│ └── order.schema.ts
|
|
├── repositories/
|
|
│ ├── user.repository.impl.ts
|
|
│ └── order.repository.impl.ts
|
|
├── adapters/
|
|
│ ├── cache/
|
|
│ │ └── redis-cache.adapter.ts
|
|
│ ├── logger/
|
|
│ │ └── winston-logger.adapter.ts
|
|
│ └── queue/
|
|
│ ├── sqs-queue.adapter.ts
|
|
│ ├── localstack-sqs.adapter.ts
|
|
│ └── fake-queue.adapter.ts
|
|
├── http/
|
|
│ ├── server/
|
|
│ │ └── hono-http-server.adapter.ts
|
|
│ ├── middleware/
|
|
│ │ ├── auth.middleware.ts
|
|
│ │ ├── validation.middleware.ts
|
|
│ │ └── error-handler.middleware.ts
|
|
│ └── plugins/
|
|
│ ├── cors.plugin.ts
|
|
│ └── openapi.plugin.ts
|
|
├── database/
|
|
│ ├── drizzle/
|
|
│ │ ├── schema/
|
|
│ │ │ └── users.schema.ts
|
|
│ │ └── migrations/
|
|
│ └── connection.ts
|
|
└── container/
|
|
└── main.ts
|
|
```
|
|
|
|
**Infrastructure Layer Responsibilities:**
|
|
|
|
- **Repositories**: Implement ports from `domain/ports/repositories/` using Drizzle ORM
|
|
- **Adapters**: Implement external service ports (Cache, Logger, Queue)
|
|
- **Controllers**: Self-registering HTTP controllers (thin layer, delegate to use cases)
|
|
- Schemas: Zod validation schemas for HTTP contracts (requests/responses)
|
|
- **HTTP Layer**: Framework-specific HTTP handling
|
|
- Server: Hono adapter (implements HttpServer port)
|
|
- Middleware: HTTP middleware (auth, validation, error handling)
|
|
- Plugins: Hono plugins (CORS, compression, OpenAPI, etc.)
|
|
- **Database**: Drizzle schemas, migrations, connection management
|
|
- **Container**: DI Container (composition root)
|
|
- **NO business logic**: Only technical implementation details
|
|
|
|
**Repository Pattern:**
|
|
|
|
```typescript
|
|
// Port in domain/ports/repositories/user.repository.ts
|
|
import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
|
|
export interface UserRepository {
|
|
save(user: User): Promise<void>;
|
|
findById(id: UUIDv7): Promise<User | undefined>;
|
|
}
|
|
|
|
// Implementation in infrastructure/repositories/user.repository.impl.ts
|
|
export class UserRepositoryImpl implements UserRepository {
|
|
// Drizzle ORM implementation
|
|
}
|
|
```
|
|
|
|
**For complete Repository and Adapter implementations with Drizzle ORM, Redis, and other infrastructure examples, see `backend-engineer` skill**
|
|
|
|
### 4. HTTP Layer (Framework-Specific, in Infrastructure)
|
|
|
|
**Purpose**: Handle HTTP requests, WebSocket connections, CLI commands
|
|
|
|
**Location**: `infrastructure/http/`
|
|
|
|
**Contains:**
|
|
|
|
- Server: Hono adapter (implements HttpServer port)
|
|
- Controllers: Self-registering HTTP controllers (route registration + handlers)
|
|
- Schemas: Zod validation for requests/responses
|
|
- Middleware: HTTP middleware (auth, validation, error handling)
|
|
- Plugins: Hono plugins (CORS, compression, OpenAPI, etc.)
|
|
|
|
**Rules:**
|
|
|
|
- ✅ Part of Infrastructure layer (HTTP is technical detail)
|
|
- ✅ Depends on Application layer (Use Cases) and HttpServer port
|
|
- ✅ Thin layer - delegates to Use Cases
|
|
- ✅ NO business logic
|
|
- ✅ Controllers auto-register routes in constructor
|
|
|
|
**Example Structure:**
|
|
|
|
```
|
|
src/infrastructure/http/
|
|
├── server/
|
|
│ └── hono-http-server.adapter.ts
|
|
├── controllers/
|
|
│ ├── user.controller.ts
|
|
│ └── order.controller.ts
|
|
├── schemas/
|
|
│ ├── user.schema.ts
|
|
│ └── order.schema.ts
|
|
├── middleware/
|
|
│ ├── auth.middleware.ts
|
|
│ └── error-handler.middleware.ts
|
|
└── plugins/
|
|
├── cors.plugin.ts
|
|
└── openapi.plugin.ts
|
|
```
|
|
|
|
**Controller Responsibilities:**
|
|
|
|
- **Thin layer**: Validation + Delegation to Use Cases
|
|
- **NO business logic**: Controllers should be lightweight
|
|
- **Request validation**: Use Zod schemas at entry point
|
|
- **Response formatting**: Return DTOs (never domain entities)
|
|
- **Self-registering**: Controllers register routes in constructor via HttpServer port
|
|
|
|
**Controller Pattern (Self-Registering):**
|
|
|
|
```typescript
|
|
// infrastructure/http/controllers/user.controller.ts
|
|
|
|
/**
|
|
* UserController
|
|
*
|
|
* Infrastructure layer (HTTP) - handles HTTP requests.
|
|
* Thin layer that delegates to use cases.
|
|
*
|
|
* Pattern: Constructor Injection + Auto-registration
|
|
*/
|
|
import type { HttpServer } from "@/domain/ports/http-server";
|
|
import { HttpMethod } from "@/domain/ports/http-server";
|
|
import type { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
|
|
|
|
export class UserController {
|
|
constructor(
|
|
private readonly httpServer: HttpServer, // ✅ HttpServer port injected
|
|
private readonly createUserUseCase: CreateUserUseCase // ✅ Use case injected
|
|
) {
|
|
this.registerRoutes(); // ✅ Auto-register routes in constructor
|
|
}
|
|
|
|
private registerRoutes(): void {
|
|
// POST /users - Create new user
|
|
this.httpServer.route(HttpMethod.POST, "/users", async (context) => {
|
|
try {
|
|
const dto = context.req.valid("json"); // Validated by middleware
|
|
const user = await this.createUserUseCase.execute(dto);
|
|
return context.json(user, 201);
|
|
} catch (error) {
|
|
console.error("Error creating user:", error);
|
|
return context.json({ error: "Internal server error" }, 500);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
**HttpServer Port (Domain Layer):**
|
|
|
|
```typescript
|
|
// domain/ports/http-server.ts
|
|
export enum HttpMethod {
|
|
GET = "GET",
|
|
POST = "POST",
|
|
PUT = "PUT",
|
|
DELETE = "DELETE",
|
|
PATCH = "PATCH",
|
|
}
|
|
|
|
export type HttpHandler = (context: unknown) => Promise<Response | unknown>;
|
|
|
|
export interface HttpServer {
|
|
route(method: HttpMethod, url: string, handler: HttpHandler): void;
|
|
listen(port: number): void;
|
|
}
|
|
```
|
|
|
|
**Key Benefits:**
|
|
|
|
- ✅ **Framework-agnostic domain** - HttpServer port in domain layer
|
|
- ✅ **Testable** - Easy to mock HttpServer for testing controllers
|
|
- ✅ **DI-friendly** - Controllers resolve via container
|
|
- ✅ **Auto-registration** - Controllers register themselves in constructor
|
|
- ✅ **Thin controllers** - Only route registration + delegation
|
|
- ✅ **Clean separation** - No routes/ folder needed
|
|
|
|
**For complete HttpServer implementation (Hono adapter), Zod validation, and middleware patterns, see `backend-engineer` skill**
|
|
|
|
## Dependency Injection
|
|
|
|
**Use custom DI Container (NO external libraries like InversifyJS or TSyringe)**
|
|
|
|
### Why Dependency Injection?
|
|
|
|
- Enables testability (inject mocks)
|
|
- Follows Dependency Inversion Principle
|
|
- Centralized dependency management
|
|
- Supports different lifetimes (singleton, scoped, transient)
|
|
|
|
### DI Principles in Clean Architecture
|
|
|
|
**Constructor Injection:**
|
|
|
|
```typescript
|
|
// ✅ Use cases depend on abstractions (ports), not implementations
|
|
export class CreateUserUseCase {
|
|
constructor(
|
|
private readonly userRepository: UserRepository, // Port from domain/ports/
|
|
private readonly passwordHasher: PasswordHasher, // Port from domain/ports/
|
|
private readonly emailService: EmailService // Port from domain/ports/
|
|
) {}
|
|
|
|
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
|
|
// Orchestrate domain logic using injected dependencies
|
|
}
|
|
}
|
|
```
|
|
|
|
**Lifetimes:**
|
|
|
|
- **singleton**: Core infrastructure (config, database, logger, repositories)
|
|
- **scoped**: Per-request instances (use cases, controllers)
|
|
- **transient**: New instance every time (rarely used)
|
|
|
|
**For complete DI Container implementation with Symbol-based tokens, registration patterns, and Hono integration, see `backend-engineer` skill**
|
|
|
|
## Testing Strategy
|
|
|
|
### Domain Layer Tests (Pure Unit Tests)
|
|
|
|
```typescript
|
|
// ✅ Easy to test - no dependencies
|
|
import { describe, expect, it } from "bun:test";
|
|
import { User } from "@/domain/entities/user.entity";
|
|
import { Email } from "@/domain/value-objects/email.value-object";
|
|
import { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
|
|
describe("User Entity", () => {
|
|
it("should deactivate user", () => {
|
|
const userId = UUIDv7.generate();
|
|
const email = Email.create("user@example.com");
|
|
const user = new User(userId, email, "John Doe", "hashed_password");
|
|
|
|
user.deactivate();
|
|
|
|
expect(user.isActive).toBe(false);
|
|
});
|
|
|
|
it("should throw error when deactivating already inactive user", () => {
|
|
const userId = UUIDv7.generate();
|
|
const email = Email.create("user@example.com");
|
|
const user = new User(userId, email, "John Doe", "hashed_password");
|
|
user.deactivate();
|
|
|
|
expect(() => user.deactivate()).toThrow();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Application Layer Tests (With Mocks)
|
|
|
|
```typescript
|
|
// ✅ Test use case with mocked ports
|
|
import { describe, expect, it, mock } from "bun:test";
|
|
import { CreateUserUseCase } from "@/application/use-cases/create-user.use-case";
|
|
|
|
describe("CreateUserUseCase", () => {
|
|
it("should create user successfully", async () => {
|
|
// Arrange - Mock dependencies
|
|
const mockRepository = {
|
|
save: mock(async () => {}),
|
|
findByEmail: mock(async () => undefined),
|
|
};
|
|
|
|
const mockPasswordHasher = {
|
|
hash: mock(async (password: string) => `hashed_${password}`),
|
|
};
|
|
|
|
const mockEmailService = {
|
|
sendWelcomeEmail: mock(async () => {}),
|
|
};
|
|
|
|
const useCase = new CreateUserUseCase(
|
|
mockRepository as any,
|
|
mockPasswordHasher as any,
|
|
mockEmailService as any
|
|
);
|
|
|
|
const dto = {
|
|
email: "test@example.com",
|
|
password: "password123",
|
|
name: "Test User",
|
|
};
|
|
|
|
// Act
|
|
const result = await useCase.execute(dto);
|
|
|
|
// Assert
|
|
expect(mockRepository.save).toHaveBeenCalledTimes(1);
|
|
expect(mockPasswordHasher.hash).toHaveBeenCalledWith("password123");
|
|
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledTimes(1);
|
|
expect(result.email).toBe("test@example.com");
|
|
});
|
|
});
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Repository Pattern
|
|
|
|
```typescript
|
|
// Port (Domain layer - domain/ports/repositories/order.repository.ts)
|
|
// NO "I" prefix
|
|
import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
|
|
export interface OrderRepository {
|
|
save(order: Order): Promise<void>;
|
|
findById(id: UUIDv7): Promise<Order | undefined>;
|
|
findByUserId(userId: UUIDv7): Promise<Order[]>;
|
|
}
|
|
|
|
// Adapter (Infrastructure layer - infrastructure/repositories/order.repository.impl.ts)
|
|
export class OrderRepositoryImpl implements OrderRepository {
|
|
async save(order: Order): Promise<void> {
|
|
// Drizzle ORM implementation
|
|
}
|
|
}
|
|
```
|
|
|
|
### Domain Service
|
|
|
|
```typescript
|
|
// ✅ Domain service when logic involves multiple entities
|
|
export class PricingService {
|
|
calculateOrderTotal(order: Order, discountRules: DiscountRule[]): Money {
|
|
let total = Money.zero();
|
|
|
|
for (const item of order.items) {
|
|
total = total.add(item.price.multiply(item.quantity));
|
|
}
|
|
|
|
for (const rule of discountRules) {
|
|
if (rule.appliesTo(order)) {
|
|
total = total.subtract(rule.calculateDiscount(total));
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Event-Driven Communication
|
|
|
|
```typescript
|
|
// Domain Event
|
|
import type { UUIDv7 } from "@/domain/value-objects/uuidv7.value-object";
|
|
import type { Email } from "@/domain/value-objects/email.value-object";
|
|
|
|
export class UserCreatedEvent {
|
|
constructor(
|
|
public readonly userId: UUIDv7,
|
|
public readonly email: Email,
|
|
public readonly occurredAt: Date = new Date()
|
|
) {}
|
|
}
|
|
|
|
// Use Case publishes event
|
|
export class CreateUserUseCase {
|
|
async execute(dto: CreateUserDto): Promise<UserResponseDto> {
|
|
// ... create user ...
|
|
|
|
await this.eventBus.publish(new UserCreatedEvent(user.id, user.email)); // user.id is UUIDv7
|
|
|
|
return UserMapper.toDto(user);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
### ❌ Anemic Domain Model
|
|
|
|
```typescript
|
|
// ❌ Bad - Just data, no behavior
|
|
export class User {
|
|
id: string;
|
|
email: string;
|
|
isActive: boolean;
|
|
}
|
|
|
|
// Business logic in service (wrong layer)
|
|
export class UserService {
|
|
deactivateUser(user: User): void {
|
|
user.isActive = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Fix:** Move behavior into entity:
|
|
|
|
```typescript
|
|
// ✅ Good - Rich domain model
|
|
export class User {
|
|
deactivate(): void {
|
|
if (!this._isActive) {
|
|
throw new UserAlreadyInactiveError(this._id);
|
|
}
|
|
this._isActive = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
### ❌ Domain Layer Depending on Infrastructure
|
|
|
|
```typescript
|
|
// ❌ Bad - Domain depends on infrastructure
|
|
import { db } from "@/infrastructure/database";
|
|
|
|
export class User {
|
|
async save(): Promise<void> {
|
|
await db.insert(users).values(this); // WRONG!
|
|
}
|
|
}
|
|
```
|
|
|
|
**Fix:** Keep domain pure, use repository:
|
|
|
|
```typescript
|
|
// ✅ Good - Pure domain, repository handles persistence
|
|
export class User {
|
|
// Pure domain logic, no database access
|
|
}
|
|
|
|
// Repository in infrastructure/repositories/
|
|
export class UserRepositoryImpl implements UserRepository {
|
|
async save(user: User): Promise<void> {
|
|
await db.insert(users).values(...);
|
|
}
|
|
}
|
|
```
|
|
|
|
### ❌ Fat Controllers
|
|
|
|
```typescript
|
|
// ❌ Bad - Business logic in controller
|
|
app.post('/users', async (c) => {
|
|
const data = c.req.valid('json');
|
|
|
|
// Validation
|
|
if (!data.email.includes('@')) {
|
|
return c.json({ error: 'Invalid email' }, 400);
|
|
}
|
|
|
|
// Check if exists
|
|
const exists = await db.select()...;
|
|
|
|
// Hash password
|
|
const hashed = await bcrypt.hash(data.password, 10);
|
|
|
|
// Save
|
|
await db.insert(users).values(...);
|
|
|
|
// Send email
|
|
await sendgrid.send(...);
|
|
|
|
return c.json(user, 201);
|
|
});
|
|
```
|
|
|
|
**Fix:** Delegate to use case:
|
|
|
|
```typescript
|
|
// ✅ Good - Thin controller
|
|
app.post("/users", zValidator("json", CreateUserSchema), async (c) => {
|
|
const dto = c.req.valid("json");
|
|
const user = await createUserUseCase.execute(dto);
|
|
return c.json(user, 201);
|
|
});
|
|
```
|
|
|
|
## Migration Strategy
|
|
|
|
### From Monolith to Clean Architecture
|
|
|
|
1. **Start with Use Cases** - Extract business logic into use cases
|
|
2. **Create Domain Models** - Move entities and value objects to domain layer
|
|
3. **Define Ports** - Create interfaces in application layer
|
|
4. **Implement Adapters** - Move infrastructure code behind interfaces
|
|
5. **Refactor Controllers** - Make them thin, delegate to use cases
|
|
|
|
## Best Practices
|
|
|
|
### Do:
|
|
|
|
- ✅ Keep domain layer pure (no external dependencies)
|
|
- ✅ Use interfaces (ports) for all external dependencies
|
|
- ✅ Implement rich domain models with behavior
|
|
- ✅ Make use cases orchestrate domain logic
|
|
- ✅ Test domain logic without infrastructure
|
|
- ✅ Use dependency injection at composition root
|
|
- ✅ Keep controllers thin (validation + delegation)
|
|
|
|
### Don't:
|
|
|
|
- ❌ Put business logic in controllers or repositories
|
|
- ❌ Let domain layer depend on infrastructure
|
|
- ❌ Create anemic domain models
|
|
- ❌ Mix layers (e.g., use Drizzle in domain layer)
|
|
- ❌ Skip interfaces (ports) for infrastructure
|
|
- ❌ Make use cases depend on concrete implementations
|
|
|
|
## Remember
|
|
|
|
- **The Dependency Rule is sacred** - Always point inward
|
|
- **Domain is the core** - Everything revolves around it
|
|
- **Test the domain first** - It's the most important part
|
|
- **Interfaces enable flexibility** - Easy to swap implementations
|
|
- **Clean Architecture is about maintainability** - Not perfection
|