feat(guardian): add guardian package - code quality analyzer

Add @puaros/guardian package v0.1.0 - code quality guardian for vibe coders and enterprise teams.

Features:
- Hardcode detection (magic numbers, magic strings)
- Circular dependency detection
- Naming convention enforcement (Clean Architecture)
- Architecture violation detection
- CLI tool with comprehensive reporting
- 159 tests with 80%+ coverage
- Smart suggestions for fixes
- Built for AI-assisted development

Built with Clean Architecture and DDD principles.
Works with Claude, GPT, Copilot, Cursor, and any AI coding assistant.
This commit is contained in:
imfozilbek
2025-11-24 02:54:39 +05:00
parent 9f97509b06
commit 03705b5264
96 changed files with 9520 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
# Guardian Examples
This directory contains examples of good and bad code patterns used for testing Guardian's detection capabilities.
## Structure
```
examples/
├── good-architecture/ # ✅ Proper Clean Architecture + DDD patterns
│ ├── domain/
│ │ ├── aggregates/ # Aggregate Roots
│ │ ├── entities/ # Domain Entities
│ │ ├── value-objects/ # Value Objects
│ │ ├── services/ # Domain Services
│ │ ├── factories/ # Domain Factories
│ │ ├── specifications/# Business Rules
│ │ └── repositories/ # Repository Interfaces
│ ├── application/
│ │ ├── use-cases/ # Application Use Cases
│ │ ├── dtos/ # Data Transfer Objects
│ │ └── mappers/ # Domain <-> DTO mappers
│ └── infrastructure/
│ ├── repositories/ # Repository Implementations
│ ├── controllers/ # HTTP Controllers
│ └── services/ # External Services
└── bad-architecture/ # ❌ Anti-patterns for testing
├── hardcoded/ # Hardcoded values
├── circular/ # Circular dependencies
├── framework-leaks/ # Framework in domain
├── entity-exposure/ # Entities in controllers
├── naming/ # Wrong naming conventions
└── anemic-model/ # Anemic domain models
```
## Patterns Demonstrated
### Domain-Driven Design (DDD)
- **Aggregate Roots**: User, Order
- **Entities**: OrderItem, Address
- **Value Objects**: Email, Money, OrderStatus
- **Domain Services**: UserRegistrationService, PricingService
- **Domain Events**: UserCreatedEvent, OrderPlacedEvent
- **Factories**: UserFactory, OrderFactory
- **Specifications**: EmailSpecification, OrderCanBeCancelledSpecification
- **Repository Interfaces**: IUserRepository, IOrderRepository
### SOLID Principles
- **SRP**: Single Responsibility - each class has one reason to change
- **OCP**: Open/Closed - extend with new classes, not modifications
- **LSP**: Liskov Substitution - derived classes are substitutable
- **ISP**: Interface Segregation - small, focused interfaces
- **DIP**: Dependency Inversion - depend on abstractions
### Clean Architecture
- **Dependency Rule**: Inner layers don't know about outer layers
- **Domain**: Pure business logic, no frameworks
- **Application**: Use cases orchestration
- **Infrastructure**: External concerns (DB, HTTP, etc.)
### Clean Code Principles
- **DRY**: Don't Repeat Yourself
- **KISS**: Keep It Simple, Stupid
- **YAGNI**: You Aren't Gonna Need It
- **Meaningful Names**: Intention-revealing names
- **Small Functions**: Do one thing well
- **No Magic Values**: Named constants
## Testing Guardian
Run Guardian on examples:
```bash
# Test good architecture (should have no violations)
pnpm guardian check examples/good-architecture
# Test bad architecture (should detect violations)
pnpm guardian check examples/bad-architecture
# Test specific anti-pattern
pnpm guardian check examples/bad-architecture/hardcoded
```
## Use Cases
### 1. Development
Use good examples as templates for new features
### 2. Testing
Use bad examples to verify Guardian detects violations
### 3. Documentation
Learn Clean Architecture + DDD patterns by example
### 4. CI/CD
Run Guardian on examples in CI to prevent regressions
---
**Note:** These examples are intentionally simplified for educational purposes. Real-world applications would have more complexity.

View File

@@ -0,0 +1,316 @@
# Guardian Examples - Summary
This document summarizes the examples created for testing Guardian's detection capabilities.
## 📁 Structure Overview
```
examples/
├── README.md # Main documentation
├── SUMMARY.md # This file
├── good-architecture/ # ✅ Best practices (29 files)
│ ├── domain/ # Domain layer (18 files)
│ │ ├── aggregates/ # User, Order aggregate roots
│ │ ├── entities/ # OrderItem entity
│ │ ├── value-objects/ # Email, Money, UserId, OrderId, OrderStatus
│ │ ├── events/ # UserCreatedEvent
│ │ ├── services/ # UserRegistrationService, PricingService
│ │ ├── factories/ # UserFactory, OrderFactory
│ │ ├── specifications/ # Specification pattern, business rules
│ │ └── repositories/ # IUserRepository, IOrderRepository interfaces
│ ├── application/ # Application layer (7 files)
│ │ ├── use-cases/ # CreateUser, PlaceOrder
│ │ ├── dtos/ # UserResponseDto, OrderResponseDto, CreateUserRequest
│ │ └── mappers/ # UserMapper, OrderMapper
│ └── infrastructure/ # Infrastructure layer (4 files)
│ ├── repositories/ # InMemoryUserRepository, InMemoryOrderRepository
│ └── controllers/ # UserController, OrderController
└── bad-architecture/ # ❌ Anti-patterns (7 files)
├── hardcoded/ # Magic numbers and strings
├── circular/ # Circular dependencies
├── framework-leaks/ # Framework in domain layer
├── entity-exposure/ # Domain entities in controllers
└── naming/ # Wrong naming conventions
```
## ✅ Good Architecture Examples (29 files)
### Domain Layer - DDD Patterns
#### 1. **Aggregates** (2 files)
- **User.ts** - User aggregate root with:
- Business operations: activate, deactivate, block, unblock, recordLogin
- Invariants validation
- Domain events (UserCreatedEvent)
- Factory methods: create(), reconstitute()
- **Order.ts** - Order aggregate with complex logic:
- Manages OrderItem entities
- Order lifecycle (confirm, pay, ship, deliver, cancel)
- Status transitions with validation
- Business rules enforcement
- Total calculation
#### 2. **Value Objects** (5 files)
- **Email.ts** - Self-validating email with regex, domain extraction
- **Money.ts** - Money with currency, arithmetic operations, prevents currency mixing
- **UserId.ts** - Strongly typed ID (UUID-based)
- **OrderId.ts** - Strongly typed Order ID
- **OrderStatus.ts** - Type-safe enum with valid transitions
#### 3. **Entities** (1 file)
- **OrderItem.ts** - Entity with identity, part of Order aggregate
#### 4. **Domain Events** (1 file)
- **UserCreatedEvent.ts** - Immutable domain event
#### 5. **Domain Services** (2 files)
- **UserRegistrationService.ts** - Checks email uniqueness, coordinates user creation
- **PricingService.ts** - Calculates discounts, shipping, tax
#### 6. **Factories** (2 files)
- **UserFactory.ts** - Creates users from OAuth, legacy data, test users
- **OrderFactory.ts** - Creates orders with various scenarios
#### 7. **Specifications** (3 files)
- **Specification.ts** - Base class with AND, OR, NOT combinators
- **EmailSpecification.ts** - Corporate email, blacklist rules
- **OrderSpecification.ts** - Discount eligibility, cancellation rules
#### 8. **Repository Interfaces** (2 files)
- **IUserRepository.ts** - User persistence abstraction
- **IOrderRepository.ts** - Order persistence abstraction
### Application Layer
#### 9. **Use Cases** (2 files)
- **CreateUser.ts** - Orchestrates user registration
- **PlaceOrder.ts** - Orchestrates order placement
#### 10. **DTOs** (3 files)
- **UserResponseDto.ts** - API response format
- **CreateUserRequest.ts** - API request format
- **OrderResponseDto.ts** - Order with items response
#### 11. **Mappers** (2 files)
- **UserMapper.ts** - Domain ↔ DTO conversion
- **OrderMapper.ts** - Domain ↔ DTO conversion
### Infrastructure Layer
#### 12. **Repositories** (2 files)
- **InMemoryUserRepository.ts** - User repository implementation
- **InMemoryOrderRepository.ts** - Order repository implementation
#### 13. **Controllers** (2 files)
- **UserController.ts** - HTTP endpoints, returns DTOs
- **OrderController.ts** - HTTP endpoints, delegates to use cases
## ❌ Bad Architecture Examples (7 files)
### 1. **Hardcoded Values** (1 file)
- **ServerWithMagicNumbers.ts**
- Magic numbers: 3000 (port), 5000 (timeout), 3 (retries), 100, 200, 60
- Magic strings: "http://localhost:8080", "mongodb://localhost:27017/mydb"
### 2. **Circular Dependencies** (2 files)
- **UserService.ts** → **OrderService.ts****UserService.ts**
- Creates circular import cycle
- Causes tight coupling
- Makes testing difficult
### 3. **Framework Leaks** (1 file)
- **UserEntity.ts**
- Imports PrismaClient in domain layer
- Violates Dependency Inversion
- Couples domain to infrastructure
### 4. **Entity Exposure** (1 file)
- **BadUserController.ts**
- Returns domain entity directly (User)
- Exposes internal structure (passwordHash, etc.)
- No DTO layer
### 5. **Naming Conventions** (2 files)
- **user.ts** - lowercase file name (should be User.ts)
- **UserDto.ts** - DTO in domain layer (should be in application)
## 🧪 Guardian Test Results
### Test 1: Good Architecture
```bash
guardian check examples/good-architecture
```
**Results:**
- ✅ No critical violations
- ⚠️ 60 hardcoded values (mostly error messages and enum values - acceptable for examples)
- ⚠️ 1 false positive: "PlaceOrder" verb not recognized (FIXED: added "Place" to allowed verbs)
**Metrics:**
- Files analyzed: 29
- Total functions: 12
- Total imports: 73
- Layer distribution:
- domain: 18 files
- application: 7 files
- infrastructure: 4 files
### Test 2: Bad Architecture
```bash
guardian check examples/bad-architecture
```
**Results:**
- ✅ Detected 9 hardcoded values in ServerWithMagicNumbers.ts
- ⚠️ Circular dependencies not detected (needs investigation)
**Detected Issues:**
1. Magic number: 3 (maxRetries)
2. Magic number: 200 (burstLimit)
3. Magic string: "mongodb://localhost:27017/mydb"
4. Magic string: "http://localhost:8080"
5. Magic string: "user@example.com"
6. Magic string: "hashed_password_exposed!"
## 📊 Patterns Demonstrated
### DDD (Domain-Driven Design)
- ✅ Aggregates: User, Order
- ✅ Entities: OrderItem
- ✅ Value Objects: Email, Money, UserId, OrderId, OrderStatus
- ✅ Domain Services: UserRegistrationService, PricingService
- ✅ Domain Events: UserCreatedEvent
- ✅ Factories: UserFactory, OrderFactory
- ✅ Specifications: Email rules, Order rules
- ✅ Repository Interfaces: IUserRepository, IOrderRepository
### SOLID Principles
-**SRP**: Each class has one responsibility
-**OCP**: Extensible through inheritance, not modification
-**LSP**: Specifications, repositories are substitutable
-**ISP**: Small, focused interfaces
-**DIP**: Domain depends on abstractions, infrastructure implements them
### Clean Architecture
-**Dependency Rule**: Domain → Application → Infrastructure
-**Boundaries**: Clear separation between layers
-**DTOs**: Application layer isolates domain from external world
-**Use Cases**: Application services orchestrate domain logic
### Clean Code Principles
-**Meaningful Names**: Email, Money, Order (not E, M, O)
-**Small Functions**: Each method does one thing
-**No Magic Values**: Named constants (MAX_RETRIES, DEFAULT_PORT)
-**DRY**: No repeated code
-**KISS**: Simple, straightforward implementations
-**YAGNI**: Only what's needed, no over-engineering
## 🎯 Key Learnings
### What Guardian Detects Well ✅
1. **Hardcoded values** - Magic numbers and strings
2. **Naming conventions** - Layer-specific patterns
3. **Layer distribution** - Clean architecture structure
4. **Project metrics** - Files, functions, imports
### What Needs Improvement ⚠️
1. **Circular dependencies** - Detection needs investigation
2. **Framework leaks** - Feature not yet implemented (v0.4.0)
3. **Entity exposure** - Feature not yet implemented (v0.4.0)
4. **False positives** - Some verbs missing from allowed list (fixed)
### What's Next (Roadmap) 🚀
1. **v0.4.0**: Framework leaks detection, Entity exposure detection
2. **v0.5.0**: Repository pattern enforcement, Dependency injection checks
3. **v0.6.0**: Over-engineering detection, Primitive obsession
4. **v0.7.0**: Configuration file support
5. **v0.8.0**: Multiple output formats (JSON, HTML, SARIF)
## 💡 How to Use These Examples
### For Learning
- Study `good-architecture/` to understand DDD and Clean Architecture
- Compare with `bad-architecture/` to see anti-patterns
- Read comments explaining WHY patterns are good or bad
### For Testing Guardian
```bash
# Test on good examples (should have minimal violations)
pnpm guardian check examples/good-architecture
# Test on bad examples (should detect violations)
pnpm guardian check examples/bad-architecture
# Test specific anti-pattern
pnpm guardian check examples/bad-architecture/hardcoded
```
### For Development
- Use good examples as templates for new features
- Add new anti-patterns to bad examples
- Test Guardian improvements against these examples
### For CI/CD
- Run Guardian on examples in CI to prevent regressions
- Ensure new Guardian versions still detect known violations
## 📝 Statistics
### Good Architecture
- **Total files**: 29
- **Domain layer**: 18 files (62%)
- **Application layer**: 7 files (24%)
- **Infrastructure layer**: 4 files (14%)
**Pattern distribution:**
- Aggregates: 2
- Value Objects: 5
- Entities: 1
- Domain Events: 1
- Domain Services: 2
- Factories: 2
- Specifications: 3
- Repositories: 2
- Use Cases: 2
- DTOs: 3
- Mappers: 2
- Controllers: 2
### Bad Architecture
- **Total files**: 7
- **Anti-patterns**: 5 categories
- **Violations detected**: 9 hardcoded values
## 🎓 Educational Value
These examples serve as:
1. **Learning material** - For understanding Clean Architecture + DDD
2. **Testing framework** - For Guardian development
3. **Documentation** - Living examples of best practices
4. **Templates** - Starting point for new projects
5. **Reference** - Quick lookup for patterns
## 🔧 Maintenance
### Adding New Examples
1. Add to appropriate directory (`good-architecture` or `bad-architecture`)
2. Follow naming conventions
3. Add detailed comments explaining patterns
4. Test with Guardian
5. Update this summary
### Testing Changes
1. Run `pnpm build` in guardian package
2. Test on both good and bad examples
3. Verify detection accuracy
4. Update SUMMARY.md with findings
---
**Last Updated**: 2025-11-24
**Guardian Version**: 0.2.0 (preparing 0.3.0)
**Examples Count**: 36 files (29 good + 7 bad)

View File

@@ -0,0 +1,34 @@
import { UserService } from "./UserService"
/**
* BAD EXAMPLE: Circular Dependency (part 2)
*
* OrderService -> UserService (creates cycle)
*/
export class OrderService {
constructor(private readonly userService: UserService) {}
public getOrdersByUser(userId: string): void {
console.warn(`Getting orders for user ${userId}`)
}
public calculateUserDiscount(userId: string): number {
const totalSpent = this.userService.getUserTotalSpent(userId)
return totalSpent > 1000 ? 0.1 : 0
}
}
/**
* ✅ GOOD VERSION:
*
* // interfaces/IOrderService.ts
* export interface IOrderService {
* getOrdersByUser(userId: string): Promise<Order[]>
* }
*
* // UserService.ts
* constructor(private readonly orderService: IOrderService) {}
*
* // OrderService.ts - no dependency on UserService
* // Use domain events or separate service for discount logic
*/

View File

@@ -0,0 +1,34 @@
import { OrderService } from "./OrderService"
/**
* BAD EXAMPLE: Circular Dependency
*
* UserService -> OrderService -> UserService
*
* Guardian should detect:
* ❌ Circular dependency cycle
*
* Why bad:
* - Tight coupling
* - Hard to test
* - Difficult to understand
* - Can cause initialization issues
* - Maintenance nightmare
*
* Fix:
* - Use interfaces
* - Use domain events
* - Extract shared logic to third service
*/
export class UserService {
constructor(private readonly orderService: OrderService) {}
public getUserOrders(userId: string): void {
console.warn(`Getting orders for user ${userId}`)
this.orderService.getOrdersByUser(userId)
}
public getUserTotalSpent(userId: string): number {
return 0
}
}

View File

@@ -0,0 +1,58 @@
/**
* BAD EXAMPLE: Entity Exposure
*
* Guardian should detect:
* ❌ Domain entity returned from controller
* ❌ No DTO layer
*
* Why bad:
* - Exposes internal structure
* - Breaking changes propagate to API
* - Can't version API independently
* - Security risk (password fields, etc.)
* - Violates Clean Architecture
*/
class User {
constructor(
public id: string,
public email: string,
public passwordHash: string,
public isAdmin: boolean,
) {}
}
export class BadUserController {
/**
* ❌ BAD: Returning domain entity directly!
*/
public async getUser(id: string): Promise<User> {
return new User(id, "user@example.com", "hashed_password_exposed!", true)
}
/**
* ❌ BAD: Accepting domain entity as input!
*/
public async updateUser(user: User): Promise<User> {
return user
}
}
/**
* ✅ GOOD VERSION:
*
* // application/dtos/UserResponseDto.ts
* export interface UserResponseDto {
* readonly id: string
* readonly email: string
* // NO password, NO internal fields
* }
*
* // infrastructure/controllers/UserController.ts
* export class UserController {
* async getUser(id: string): Promise<UserResponseDto> {
* const user = await this.getUserUseCase.execute(id)
* return UserMapper.toDto(user) // Convert to DTO!
* }
* }
*/

View File

@@ -0,0 +1,62 @@
/**
* BAD EXAMPLE: Framework Leak in Domain Layer
*
* Guardian should detect:
* ❌ Prisma import in domain layer
* ❌ Framework dependency in domain
*
* Why bad:
* - Domain coupled to infrastructure
* - Hard to test
* - Can't change DB without changing domain
* - Violates Dependency Inversion Principle
* - Violates Clean Architecture
*/
// ❌ BAD: Framework in domain!
import { PrismaClient } from "@prisma/client"
export class UserEntity {
constructor(
public id: string,
public email: string,
private readonly prisma: PrismaClient,
) {}
public async save(): Promise<void> {
await this.prisma.user.create({
data: {
id: this.id,
email: this.email,
},
})
}
}
/**
* ✅ GOOD VERSION:
*
* // domain/entities/User.ts - NO framework imports!
* export class User {
* constructor(
* private readonly id: UserId,
* private readonly email: Email,
* ) {}
*
* // No persistence logic here
* }
*
* // domain/repositories/IUserRepository.ts
* export interface IUserRepository {
* save(user: User): Promise<void>
* }
*
* // infrastructure/repositories/PrismaUserRepository.ts
* export class PrismaUserRepository implements IUserRepository {
* constructor(private readonly prisma: PrismaClient) {}
*
* async save(user: User): Promise<void> {
* // Prisma code here
* }
* }
*/

View File

@@ -0,0 +1,67 @@
/**
* BAD EXAMPLE: Hardcoded values
*
* Guardian should detect:
* ❌ Magic number: 3000 (port)
* ❌ Magic number: 5000 (timeout)
* ❌ Magic number: 3 (max retries)
* ❌ Magic string: "http://localhost:8080" (API URL)
* ❌ Magic string: "mongodb://localhost:27017/mydb" (DB connection)
*
* Why bad:
* - Hard to maintain
* - Can't configure per environment
* - Scattered across codebase
* - No single source of truth
*/
export class ServerWithMagicNumbers {
public startServer(): void {
console.warn("Starting server on port 3000")
setTimeout(() => {
console.warn("Server timeout after 5000ms")
}, 5000)
}
public connectToDatabase(): void {
const connectionString = "mongodb://localhost:27017/mydb"
console.warn(`Connecting to: ${connectionString}`)
}
public async fetchDataWithRetry(): Promise<void> {
const apiUrl = "http://localhost:8080"
let attempts = 0
const maxRetries = 3
while (attempts < maxRetries) {
try {
console.warn(`Fetching from ${apiUrl}`)
break
} catch (error) {
attempts++
}
}
}
public configureRateLimits(): void {
const requestsPerMinute = 100
const burstLimit = 200
const windowSizeSeconds = 60
console.warn(
`Rate limits: ${requestsPerMinute} per ${windowSizeSeconds}s, burst: ${burstLimit}`,
)
}
}
/**
* ✅ GOOD VERSION (for comparison):
*
* const DEFAULT_PORT = 3000
* const TIMEOUT_MS = 5000
* const MAX_RETRIES = 3
* const API_BASE_URL = "http://localhost:8080"
* const DB_CONNECTION_STRING = "mongodb://localhost:27017/mydb"
* const REQUESTS_PER_MINUTE = 100
*/

View File

@@ -0,0 +1,29 @@
/**
* BAD EXAMPLE: DTO in Domain Layer
*
* Guardian should detect:
* ❌ DTO in domain layer
* ❌ DTOs belong in application or infrastructure
*
* Why bad:
* - Domain should have entities and value objects
* - DTOs are for external communication
* - Violates layer responsibilities
*/
export class UserDto {
constructor(
public id: string,
public email: string,
) {}
}
/**
* ✅ GOOD VERSION:
*
* // domain/entities/User.ts - Entity in domain
* export class User { ... }
*
* // application/dtos/UserDto.ts - DTO in application
* export interface UserDto { ... }
*/

View File

@@ -0,0 +1,29 @@
/**
* BAD EXAMPLE: Naming Convention Violations
*
* Guardian should detect:
* ❌ File name: user.ts (should be PascalCase: User.ts)
* ❌ Location: application layer use case should start with verb
*
* Why bad:
* - Inconsistent naming
* - Hard to find files
* - Not following Clean Architecture conventions
*/
export class user {
constructor(
public id: string,
public email: string,
) {}
}
/**
* ✅ GOOD VERSION:
*
* // domain/entities/User.ts - PascalCase entity
* export class User { ... }
*
* // application/use-cases/CreateUser.ts - Verb+Noun
* export class CreateUser { ... }
*/

View File

@@ -0,0 +1,13 @@
/**
* Create User Request DTO
*
* Application Layer: Input DTO
* - Validation at system boundary
* - No domain logic
* - API contract
*/
export interface CreateUserRequest {
readonly email: string
readonly firstName: string
readonly lastName: string
}

View File

@@ -0,0 +1,24 @@
/**
* Order Response DTO
*/
export interface OrderItemDto {
readonly id: string
readonly productId: string
readonly productName: string
readonly price: number
readonly currency: string
readonly quantity: number
readonly total: number
}
export interface OrderResponseDto {
readonly id: string
readonly userId: string
readonly items: OrderItemDto[]
readonly status: string
readonly subtotal: number
readonly currency: string
readonly createdAt: string
readonly confirmedAt?: string
readonly deliveredAt?: string
}

View File

@@ -0,0 +1,33 @@
/**
* User Response DTO
*
* DDD Pattern: Data Transfer Object
* - No business logic
* - Presentation layer data structure
* - Protects domain from external changes
*
* SOLID Principles:
* - SRP: only data transfer
* - ISP: client-specific interface
*
* Clean Architecture:
* - Application layer DTO
* - Maps to/from domain
* - API contracts
*
* Benefits:
* - Domain entity isolation
* - API versioning
* - Client-specific data
*/
export interface UserResponseDto {
readonly id: string
readonly email: string
readonly firstName: string
readonly lastName: string
readonly fullName: string
readonly isActive: boolean
readonly isBlocked: boolean
readonly registeredAt: string
readonly lastLoginAt?: string
}

View File

@@ -0,0 +1,41 @@
import { Order } from "../../domain/aggregates/Order"
import { OrderItemDto, OrderResponseDto } from "../dtos/OrderResponseDto"
/**
* Order Mapper
*/
export class OrderMapper {
public static toDto(order: Order): OrderResponseDto {
const total = order.calculateTotal()
return {
id: order.orderId.value,
userId: order.userId.value,
items: order.items.map((item) => OrderMapper.toItemDto(item)),
status: order.status.value,
subtotal: total.amount,
currency: total.currency,
createdAt: order.createdAt.toISOString(),
confirmedAt: order.confirmedAt?.toISOString(),
deliveredAt: order.deliveredAt?.toISOString(),
}
}
private static toItemDto(item: any): OrderItemDto {
const total = item.calculateTotal()
return {
id: item.id,
productId: item.productId,
productName: item.productName,
price: item.price.amount,
currency: item.price.currency,
quantity: item.quantity,
total: total.amount,
}
}
public static toDtoList(orders: Order[]): OrderResponseDto[] {
return orders.map((order) => OrderMapper.toDto(order))
}
}

View File

@@ -0,0 +1,44 @@
import { User } from "../../domain/aggregates/User"
import { UserResponseDto } from "../dtos/UserResponseDto"
/**
* User Mapper
*
* DDD Pattern: Mapper
* - Converts between domain and DTOs
* - Isolates domain from presentation
* - No business logic
*
* SOLID Principles:
* - SRP: only mapping
* - OCP: extend for new DTOs
*
* Clean Architecture:
* - Application layer
* - Protects domain integrity
*/
export class UserMapper {
/**
* Map domain entity to response DTO
*/
public static toDto(user: User): UserResponseDto {
return {
id: user.userId.value,
email: user.email.value,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
isActive: user.isActive,
isBlocked: user.isBlocked,
registeredAt: user.registeredAt.toISOString(),
lastLoginAt: user.lastLoginAt?.toISOString(),
}
}
/**
* Map array of entities to DTOs
*/
public static toDtoList(users: User[]): UserResponseDto[] {
return users.map((user) => UserMapper.toDto(user))
}
}

View File

@@ -0,0 +1,60 @@
import { Email } from "../../domain/value-objects/Email"
import { UserRegistrationService } from "../../domain/services/UserRegistrationService"
import { UserMapper } from "../mappers/UserMapper"
import { CreateUserRequest } from "../dtos/CreateUserRequest"
import { UserResponseDto } from "../dtos/UserResponseDto"
/**
* Use Case: CreateUser
*
* DDD Pattern: Application Service / Use Case
* - Orchestrates domain operations
* - Transaction boundary
* - Converts DTOs to domain
*
* SOLID Principles:
* - SRP: handles user creation workflow
* - DIP: depends on abstractions (UserRegistrationService)
* - OCP: can extend without modifying
*
* Clean Architecture:
* - Application layer
* - Uses domain services
* - Returns DTOs (not domain entities)
*
* Clean Code:
* - Verb+Noun naming: CreateUser
* - Single purpose
* - No business logic (delegated to domain)
*/
export class CreateUser {
constructor(private readonly userRegistrationService: UserRegistrationService) {}
public async execute(request: CreateUserRequest): Promise<UserResponseDto> {
this.validateRequest(request)
const email = Email.create(request.email)
const user = await this.userRegistrationService.registerUser(
email,
request.firstName,
request.lastName,
)
return UserMapper.toDto(user)
}
private validateRequest(request: CreateUserRequest): void {
if (!request.email?.trim()) {
throw new Error("Email is required")
}
if (!request.firstName?.trim()) {
throw new Error("First name is required")
}
if (!request.lastName?.trim()) {
throw new Error("Last name is required")
}
}
}

View File

@@ -0,0 +1,88 @@
import { OrderFactory } from "../../domain/factories/OrderFactory"
import { IOrderRepository } from "../../domain/repositories/IOrderRepository"
import { UserId } from "../../domain/value-objects/UserId"
import { Money } from "../../domain/value-objects/Money"
import { OrderMapper } from "../mappers/OrderMapper"
import { OrderResponseDto } from "../dtos/OrderResponseDto"
/**
* Place Order Request
*/
export interface PlaceOrderRequest {
readonly userId: string
readonly items: Array<{
readonly productId: string
readonly productName: string
readonly price: number
readonly currency: string
readonly quantity: number
}>
}
/**
* Use Case: PlaceOrder
*
* Application Service:
* - Orchestrates order placement
* - Transaction boundary
* - Validation at system boundary
*
* Business Flow:
* 1. Validate request
* 2. Create order with items
* 3. Confirm order
* 4. Persist order
* 5. Return DTO
*/
export class PlaceOrder {
constructor(private readonly orderRepository: IOrderRepository) {}
public async execute(request: PlaceOrderRequest): Promise<OrderResponseDto> {
this.validateRequest(request)
const userId = UserId.create(request.userId)
const items = request.items.map((item) => ({
productId: item.productId,
productName: item.productName,
price: Money.create(item.price, item.currency),
quantity: item.quantity,
}))
const order = OrderFactory.createWithItems(userId, items)
order.confirm()
await this.orderRepository.save(order)
return OrderMapper.toDto(order)
}
private validateRequest(request: PlaceOrderRequest): void {
if (!request.userId?.trim()) {
throw new Error("User ID is required")
}
if (!request.items || request.items.length === 0) {
throw new Error("Order must have at least one item")
}
for (const item of request.items) {
if (!item.productId?.trim()) {
throw new Error("Product ID is required")
}
if (!item.productName?.trim()) {
throw new Error("Product name is required")
}
if (item.price <= 0) {
throw new Error("Price must be positive")
}
if (item.quantity <= 0) {
throw new Error("Quantity must be positive")
}
}
}
}

View File

@@ -0,0 +1,263 @@
import { BaseEntity } from "../../../../src/domain/entities/BaseEntity"
import { OrderId } from "../value-objects/OrderId"
import { UserId } from "../value-objects/UserId"
import { OrderStatus } from "../value-objects/OrderStatus"
import { Money } from "../value-objects/Money"
import { OrderItem } from "../entities/OrderItem"
/**
* Order Aggregate Root
*
* DDD Patterns:
* - Aggregate Root: controls access to OrderItems
* - Consistency Boundary: all changes through Order
* - Rich Domain Model: contains business logic
*
* SOLID Principles:
* - SRP: manages order lifecycle
* - OCP: extensible through status transitions
* - ISP: focused interface for order operations
*
* Business Rules (Invariants):
* - Order must have at least one item
* - Cannot modify confirmed/paid/shipped orders
* - Status transitions must be valid
* - Total = sum of all items
* - Cannot cancel delivered orders
*
* Clean Code:
* - No magic numbers: MIN_ITEMS constant
* - Meaningful names: addItem, removeItem, confirm
* - Small methods: each does one thing
* - No hardcoded strings: OrderStatus enum
*/
export class Order extends BaseEntity {
private static readonly MIN_ITEMS = 1
private readonly _orderId: OrderId
private readonly _userId: UserId
private readonly _items: Map<string, OrderItem>
private _status: OrderStatus
private readonly _createdAt: Date
private _confirmedAt?: Date
private _deliveredAt?: Date
private constructor(
orderId: OrderId,
userId: UserId,
items: OrderItem[],
status: OrderStatus,
createdAt: Date,
confirmedAt?: Date,
deliveredAt?: Date,
) {
super(orderId.value)
this._orderId = orderId
this._userId = userId
this._items = new Map(items.map((item) => [item.id, item]))
this._status = status
this._createdAt = createdAt
this._confirmedAt = confirmedAt
this._deliveredAt = deliveredAt
this.validateInvariants()
}
/**
* Factory: Create new order
*/
public static create(userId: UserId): Order {
const orderId = OrderId.create()
const now = new Date()
return new Order(orderId, userId, [], OrderStatus.PENDING, now)
}
/**
* Factory: Reconstitute from persistence
*/
public static reconstitute(
orderId: OrderId,
userId: UserId,
items: OrderItem[],
status: OrderStatus,
createdAt: Date,
confirmedAt?: Date,
deliveredAt?: Date,
): Order {
return new Order(orderId, userId, items, status, createdAt, confirmedAt, deliveredAt)
}
/**
* Business Operation: Add item to order
*
* DDD: Only Aggregate Root can modify its entities
*/
public addItem(productId: string, productName: string, price: Money, quantity: number): void {
this.ensureCanModify()
const existingItem = Array.from(this._items.values()).find(
(item) => item.productId === productId,
)
if (existingItem) {
existingItem.updateQuantity(existingItem.quantity + quantity)
} else {
const newItem = OrderItem.create(productId, productName, price, quantity)
this._items.set(newItem.id, newItem)
}
this.touch()
}
/**
* Business Operation: Remove item from order
*/
public removeItem(itemId: string): void {
this.ensureCanModify()
if (!this._items.has(itemId)) {
throw new Error(`Item not found: ${itemId}`)
}
this._items.delete(itemId)
this.touch()
}
/**
* Business Operation: Update item quantity
*/
public updateItemQuantity(itemId: string, newQuantity: number): void {
this.ensureCanModify()
const item = this._items.get(itemId)
if (!item) {
throw new Error(`Item not found: ${itemId}`)
}
item.updateQuantity(newQuantity)
this.touch()
}
/**
* Business Operation: Confirm order
*/
public confirm(): void {
this.transitionTo(OrderStatus.CONFIRMED)
this._confirmedAt = new Date()
}
/**
* Business Operation: Mark as paid
*/
public markAsPaid(): void {
this.transitionTo(OrderStatus.PAID)
}
/**
* Business Operation: Ship order
*/
public ship(): void {
this.transitionTo(OrderStatus.SHIPPED)
}
/**
* Business Operation: Deliver order
*/
public deliver(): void {
this.transitionTo(OrderStatus.DELIVERED)
this._deliveredAt = new Date()
}
/**
* Business Operation: Cancel order
*/
public cancel(): void {
if (this._status.isDelivered()) {
throw new Error("Cannot cancel delivered order")
}
this.transitionTo(OrderStatus.CANCELLED)
}
/**
* Business Query: Calculate total
*/
public calculateTotal(): Money {
const items = Array.from(this._items.values())
if (items.length === 0) {
return Money.zero("USD")
}
return items.reduce((total, item) => total.add(item.calculateTotal()), Money.zero("USD"))
}
/**
* Business Query: Check if order can be modified
*/
public canModify(): boolean {
return this._status.isPending()
}
/**
* Getters
*/
public get orderId(): OrderId {
return this._orderId
}
public get userId(): UserId {
return this._userId
}
public get items(): readonly OrderItem[] {
return Array.from(this._items.values())
}
public get status(): OrderStatus {
return this._status
}
public get createdAt(): Date {
return this._createdAt
}
public get confirmedAt(): Date | undefined {
return this._confirmedAt
}
public get deliveredAt(): Date | undefined {
return this._deliveredAt
}
/**
* Private helpers
*/
private ensureCanModify(): void {
if (!this.canModify()) {
throw new Error(`Cannot modify order in ${this._status.value} status`)
}
}
private transitionTo(newStatus: OrderStatus): void {
if (!this._status.canTransitionTo(newStatus)) {
throw new Error(
`Invalid status transition: ${this._status.value} -> ${newStatus.value}`,
)
}
this._status = newStatus
this.touch()
}
/**
* Invariant validation
*/
private validateInvariants(): void {
if (!this._status.isPending() && this._items.size < Order.MIN_ITEMS) {
throw new Error(`Order must have at least ${Order.MIN_ITEMS} item(s)`)
}
}
}

View File

@@ -0,0 +1,251 @@
import { BaseEntity } from "../../../../src/domain/entities/BaseEntity"
import { Email } from "../value-objects/Email"
import { UserId } from "../value-objects/UserId"
import { UserCreatedEvent } from "../events/UserCreatedEvent"
/**
* User Aggregate Root
*
* DDD Patterns:
* - Aggregate Root: consistency boundary
* - Rich Domain Model: contains business logic
* - Domain Events: publishes UserCreatedEvent
*
* SOLID Principles:
* - SRP: manages user identity and state
* - OCP: extensible through events
* - DIP: depends on abstractions (Email, UserId)
*
* Business Rules (Invariants):
* - Email must be unique (enforced by repository)
* - User must have valid email
* - Blocked users cannot be activated directly
* - Only active users can be blocked
*/
export class User extends BaseEntity {
private readonly _userId: UserId
private readonly _email: Email
private readonly _firstName: string
private readonly _lastName: string
private _isActive: boolean
private _isBlocked: boolean
private readonly _registeredAt: Date
private _lastLoginAt?: Date
private constructor(
userId: UserId,
email: Email,
firstName: string,
lastName: string,
isActive: boolean,
isBlocked: boolean,
registeredAt: Date,
lastLoginAt?: Date,
) {
super(userId.value)
this._userId = userId
this._email = email
this._firstName = firstName
this._lastName = lastName
this._isActive = isActive
this._isBlocked = isBlocked
this._registeredAt = registeredAt
this._lastLoginAt = lastLoginAt
this.validateInvariants()
}
/**
* Factory method: Create new user (business operation)
*
* DDD: Named constructor that represents business intent
* Clean Code: Intention-revealing method name
*/
public static create(email: Email, firstName: string, lastName: string): User {
const userId = UserId.create()
const now = new Date()
const user = new User(userId, email, firstName, lastName, true, false, now)
user.addDomainEvent(
new UserCreatedEvent({
userId: userId.value,
email: email.value,
registeredAt: now,
}),
)
return user
}
/**
* Factory method: Reconstitute from persistence
*
* DDD: Separate creation from reconstitution
* No events raised - already happened
*/
public static reconstitute(
userId: UserId,
email: Email,
firstName: string,
lastName: string,
isActive: boolean,
isBlocked: boolean,
registeredAt: Date,
lastLoginAt?: Date,
): User {
return new User(
userId,
email,
firstName,
lastName,
isActive,
isBlocked,
registeredAt,
lastLoginAt,
)
}
/**
* Business Operation: Activate user
*
* DDD: Business logic in domain
* SOLID SRP: User manages its own state
*/
public activate(): void {
if (this._isBlocked) {
throw new Error("Cannot activate blocked user. Unblock first.")
}
if (this._isActive) {
return
}
this._isActive = true
this.touch()
}
/**
* Business Operation: Deactivate user
*/
public deactivate(): void {
if (!this._isActive) {
return
}
this._isActive = false
this.touch()
}
/**
* Business Operation: Block user
*
* Business Rule: Only active users can be blocked
*/
public block(reason: string): void {
if (!this._isActive) {
throw new Error("Cannot block inactive user")
}
if (this._isBlocked) {
return
}
this._isBlocked = true
this._isActive = false
this.touch()
}
/**
* Business Operation: Unblock user
*/
public unblock(): void {
if (!this._isBlocked) {
return
}
this._isBlocked = false
this.touch()
}
/**
* Business Operation: Record login
*/
public recordLogin(): void {
if (!this._isActive) {
throw new Error("Inactive user cannot login")
}
if (this._isBlocked) {
throw new Error("Blocked user cannot login")
}
this._lastLoginAt = new Date()
this.touch()
}
/**
* Business Query: Check if user can login
*/
public canLogin(): boolean {
return this._isActive && !this._isBlocked
}
/**
* Getters: Read-only access to state
*/
public get userId(): UserId {
return this._userId
}
public get email(): Email {
return this._email
}
public get firstName(): string {
return this._firstName
}
public get lastName(): string {
return this._lastName
}
public get fullName(): string {
return `${this._firstName} ${this._lastName}`
}
public get isActive(): boolean {
return this._isActive
}
public get isBlocked(): boolean {
return this._isBlocked
}
public get registeredAt(): Date {
return this._registeredAt
}
public get lastLoginAt(): Date | undefined {
return this._lastLoginAt
}
/**
* Invariant validation
*
* DDD: Enforce business rules
*/
private validateInvariants(): void {
if (!this._firstName?.trim()) {
throw new Error("First name is required")
}
if (!this._lastName?.trim()) {
throw new Error("Last name is required")
}
if (this._isBlocked && this._isActive) {
throw new Error("Blocked user cannot be active")
}
}
}

View File

@@ -0,0 +1,101 @@
import { BaseEntity } from "../../../../src/domain/entities/BaseEntity"
import { Money } from "../value-objects/Money"
/**
* OrderItem Entity
*
* DDD Pattern: Entity (not Aggregate Root)
* - Has identity
* - Part of Order aggregate
* - Cannot exist without Order
* - Accessed only through Order
*
* Business Rules:
* - Quantity must be positive
* - Price must be positive
* - Total = price * quantity
*/
export class OrderItem extends BaseEntity {
private readonly _productId: string
private readonly _productName: string
private readonly _price: Money
private _quantity: number
private constructor(
productId: string,
productName: string,
price: Money,
quantity: number,
id?: string,
) {
super(id)
this._productId = productId
this._productName = productName
this._price = price
this._quantity = quantity
this.validateInvariants()
}
public static create(
productId: string,
productName: string,
price: Money,
quantity: number,
): OrderItem {
return new OrderItem(productId, productName, price, quantity)
}
public static reconstitute(
productId: string,
productName: string,
price: Money,
quantity: number,
id: string,
): OrderItem {
return new OrderItem(productId, productName, price, quantity, id)
}
public updateQuantity(newQuantity: number): void {
if (newQuantity <= 0) {
throw new Error("Quantity must be positive")
}
this._quantity = newQuantity
this.touch()
}
public calculateTotal(): Money {
return this._price.multiply(this._quantity)
}
public get productId(): string {
return this._productId
}
public get productName(): string {
return this._productName
}
public get price(): Money {
return this._price
}
public get quantity(): number {
return this._quantity
}
private validateInvariants(): void {
if (!this._productId?.trim()) {
throw new Error("Product ID is required")
}
if (!this._productName?.trim()) {
throw new Error("Product name is required")
}
if (this._quantity <= 0) {
throw new Error("Quantity must be positive")
}
}
}

View File

@@ -0,0 +1,27 @@
import { DomainEvent } from "../../../../src/domain/events/DomainEvent"
/**
* Domain Event: UserCreatedEvent
*
* DDD Pattern: Domain Events
* - Represents something that happened in the domain
* - Immutable
* - Past tense naming
*
* Use cases:
* - Send welcome email (async)
* - Create user profile
* - Log user registration
* - Analytics tracking
*/
export interface UserCreatedEventPayload {
readonly userId: string
readonly email: string
readonly registeredAt: Date
}
export class UserCreatedEvent extends DomainEvent<UserCreatedEventPayload> {
constructor(payload: UserCreatedEventPayload) {
super("user.created", payload)
}
}

View File

@@ -0,0 +1,102 @@
import { Order } from "../aggregates/Order"
import { OrderId } from "../value-objects/OrderId"
import { UserId } from "../value-objects/UserId"
import { OrderStatus } from "../value-objects/OrderStatus"
import { OrderItem } from "../entities/OrderItem"
import { Money } from "../value-objects/Money"
/**
* Factory: OrderFactory
*
* DDD Pattern: Factory
* - Handles complex Order creation
* - Different creation scenarios
* - Validation and defaults
*
* Clean Code:
* - Each method has clear purpose
* - No magic values
* - Meaningful names
*/
export class OrderFactory {
/**
* Create empty order for user
*/
public static createEmptyOrder(userId: UserId): Order {
return Order.create(userId)
}
/**
* Create order with initial items
*/
public static createWithItems(
userId: UserId,
items: Array<{ productId: string; productName: string; price: Money; quantity: number }>,
): Order {
const order = Order.create(userId)
for (const item of items) {
order.addItem(item.productId, item.productName, item.price, item.quantity)
}
return order
}
/**
* Reconstitute order from persistence
*/
public static reconstitute(data: {
orderId: string
userId: string
items: Array<{
id: string
productId: string
productName: string
price: number
currency: string
quantity: number
}>
status: string
createdAt: Date
confirmedAt?: Date
deliveredAt?: Date
}): Order {
const orderId = OrderId.create(data.orderId)
const userId = UserId.create(data.userId)
const status = OrderStatus.create(data.status)
const items = data.items.map((item) =>
OrderItem.reconstitute(
item.productId,
item.productName,
Money.create(item.price, item.currency),
item.quantity,
item.id,
),
)
return Order.reconstitute(
orderId,
userId,
items,
status,
data.createdAt,
data.confirmedAt,
data.deliveredAt,
)
}
/**
* Create test order
*/
public static createTestOrder(userId?: UserId): Order {
const testUserId = userId ?? UserId.create()
const order = Order.create(testUserId)
order.addItem("test-product-1", "Test Product 1", Money.create(10, "USD"), 2)
order.addItem("test-product-2", "Test Product 2", Money.create(20, "USD"), 1)
return order
}
}

View File

@@ -0,0 +1,77 @@
import { User } from "../aggregates/User"
import { Email } from "../value-objects/Email"
import { UserId } from "../value-objects/UserId"
/**
* Factory: UserFactory
*
* DDD Pattern: Factory
* - Encapsulates complex object creation
* - Hides construction details
* - Can create from different sources
*
* SOLID Principles:
* - SRP: responsible only for creating Users
* - OCP: can add new creation methods
* - DIP: returns domain object, not DTO
*
* Use cases:
* - Create from external auth provider (OAuth, SAML)
* - Create from legacy data
* - Create with default values
* - Create test users
*/
export class UserFactory {
/**
* Create user from OAuth provider data
*/
public static createFromOAuth(
oauthEmail: string,
oauthFirstName: string,
oauthLastName: string,
): User {
const email = Email.create(oauthEmail)
const firstName = oauthFirstName.trim() || "Unknown"
const lastName = oauthLastName.trim() || "User"
return User.create(email, firstName, lastName)
}
/**
* Create user from legacy database format
*/
public static createFromLegacy(legacyData: {
id: string
email: string
full_name: string
active: number
created_timestamp: number
}): User {
const [firstName = "Unknown", lastName = "User"] = legacyData.full_name.split(" ")
const userId = UserId.create(legacyData.id)
const email = Email.create(legacyData.email)
const isActive = legacyData.active === 1
const registeredAt = new Date(legacyData.created_timestamp * 1000)
return User.reconstitute(userId, email, firstName, lastName, isActive, false, registeredAt)
}
/**
* Create test user with defaults
*/
public static createTestUser(emailSuffix: string = "test"): User {
const email = Email.create(`test-${Date.now()}@${emailSuffix}.com`)
return User.create(email, "Test", "User")
}
/**
* Create admin user
*/
public static createAdmin(email: Email, firstName: string, lastName: string): User {
const user = User.create(email, firstName, lastName)
user.activate()
return user
}
}

View File

@@ -0,0 +1,48 @@
import { Order } from "../aggregates/Order"
import { OrderId } from "../value-objects/OrderId"
import { UserId } from "../value-objects/UserId"
/**
* Order Repository Interface
*
* DDD Pattern: Repository
* - Aggregate-oriented persistence
* - Collection metaphor
* - No business logic (that's in Order aggregate)
*/
export interface IOrderRepository {
/**
* Save order (create or update)
*/
save(order: Order): Promise<void>
/**
* Find order by ID
*/
findById(id: OrderId): Promise<Order | null>
/**
* Find orders by user
*/
findByUserId(userId: UserId): Promise<Order[]>
/**
* Find orders by status
*/
findByStatus(status: string): Promise<Order[]>
/**
* Find all orders
*/
findAll(): Promise<Order[]>
/**
* Delete order
*/
delete(id: OrderId): Promise<void>
/**
* Check if order exists
*/
exists(id: OrderId): Promise<boolean>
}

View File

@@ -0,0 +1,57 @@
import { User } from "../aggregates/User"
import { UserId } from "../value-objects/UserId"
import { Email } from "../value-objects/Email"
/**
* User Repository Interface
*
* DDD Pattern: Repository
* - Interface in domain layer
* - Implementation in infrastructure layer
* - Collection-like API for aggregates
*
* SOLID Principles:
* - DIP: domain depends on abstraction
* - ISP: focused interface
* - SRP: manages User persistence
*
* Clean Architecture:
* - Domain doesn't know about DB
* - Infrastructure implements this
*/
export interface IUserRepository {
/**
* Save user (create or update)
*/
save(user: User): Promise<void>
/**
* Find user by ID
*/
findById(id: UserId): Promise<User | null>
/**
* Find user by email
*/
findByEmail(email: Email): Promise<User | null>
/**
* Find all users
*/
findAll(): Promise<User[]>
/**
* Find active users
*/
findActive(): Promise<User[]>
/**
* Delete user
*/
delete(id: UserId): Promise<void>
/**
* Check if user exists
*/
exists(id: UserId): Promise<boolean>
}

View File

@@ -0,0 +1,80 @@
import { Order } from "../aggregates/Order"
import { Money } from "../value-objects/Money"
/**
* Domain Service: PricingService
*
* DDD Pattern: Domain Service
* - Encapsulates pricing business logic
* - Pure business logic (no infrastructure)
* - Can be used by multiple aggregates
*
* Business Rules:
* - Discounts based on order total
* - Free shipping threshold
* - Tax calculation
*
* Clean Code:
* - No magic numbers: constants for thresholds
* - Clear method names
* - Single Responsibility
*/
export class PricingService {
private static readonly DISCOUNT_THRESHOLD = Money.create(100, "USD")
private static readonly DISCOUNT_PERCENTAGE = 0.1
private static readonly FREE_SHIPPING_THRESHOLD = Money.create(50, "USD")
private static readonly SHIPPING_COST = Money.create(10, "USD")
private static readonly TAX_RATE = 0.2
/**
* Calculate discount for order
*
* Business Rule: 10% discount for orders over $100
*/
public calculateDiscount(order: Order): Money {
const total = order.calculateTotal()
if (total.isGreaterThan(PricingService.DISCOUNT_THRESHOLD)) {
return total.multiply(PricingService.DISCOUNT_PERCENTAGE)
}
return Money.zero(total.currency)
}
/**
* Calculate shipping cost
*
* Business Rule: Free shipping for orders over $50
*/
public calculateShippingCost(order: Order): Money {
const total = order.calculateTotal()
if (total.isGreaterThan(PricingService.FREE_SHIPPING_THRESHOLD)) {
return Money.zero(total.currency)
}
return PricingService.SHIPPING_COST
}
/**
* Calculate tax
*
* Business Rule: 20% tax on order total
*/
public calculateTax(order: Order): Money {
const total = order.calculateTotal()
return total.multiply(PricingService.TAX_RATE)
}
/**
* Calculate final total with all costs
*/
public calculateFinalTotal(order: Order): Money {
const subtotal = order.calculateTotal()
const discount = this.calculateDiscount(order)
const shipping = this.calculateShippingCost(order)
const tax = this.calculateTax(order)
return subtotal.subtract(discount).add(shipping).add(tax)
}
}

View File

@@ -0,0 +1,63 @@
import { User } from "../aggregates/User"
import { Email } from "../value-objects/Email"
import { IUserRepository } from "../repositories/IUserRepository"
/**
* Domain Service: UserRegistrationService
*
* DDD Pattern: Domain Service
* - Encapsulates business logic that doesn't belong to a single entity
* - Coordinates multiple aggregates
* - Stateless
*
* When to use Domain Service:
* - Business logic spans multiple aggregates
* - Operation doesn't naturally fit in any entity
* - Need to check uniqueness (requires repository)
*
* SOLID Principles:
* - SRP: handles user registration logic
* - DIP: depends on IUserRepository abstraction
* - ISP: focused interface
*
* Clean Code:
* - Meaningful name: clearly registration logic
* - Small method: does one thing
* - No magic strings: clear error messages
*/
export class UserRegistrationService {
constructor(private readonly userRepository: IUserRepository) {}
/**
* Business Operation: Register new user
*
* Business Rules:
* - Email must be unique
* - User must have valid data
* - Registration creates active user
*
* @throws Error if email already exists
* @throws Error if user data is invalid
*/
public async registerUser(email: Email, firstName: string, lastName: string): Promise<User> {
const existingUser = await this.userRepository.findByEmail(email)
if (existingUser) {
throw new Error(`User with email ${email.value} already exists`)
}
const user = User.create(email, firstName, lastName)
await this.userRepository.save(user)
return user
}
/**
* Business Query: Check if email is available
*/
public async isEmailAvailable(email: Email): boolean {
const existingUser = await this.userRepository.findByEmail(email)
return !existingUser
}
}

View File

@@ -0,0 +1,52 @@
import { Specification } from "./Specification"
import { Email } from "../value-objects/Email"
/**
* Email Domain Specification
*
* Business Rule: Check if email is from corporate domain
*/
export class CorporateEmailSpecification extends Specification<Email> {
private static readonly CORPORATE_DOMAINS = ["company.com", "corp.company.com"]
public isSatisfiedBy(email: Email): boolean {
const domain = email.getDomain()
return CorporateEmailSpecification.CORPORATE_DOMAINS.includes(domain)
}
}
/**
* Email Blacklist Specification
*
* Business Rule: Check if email domain is blacklisted
*/
export class BlacklistedEmailSpecification extends Specification<Email> {
private static readonly BLACKLISTED_DOMAINS = [
"tempmail.com",
"throwaway.email",
"guerrillamail.com",
]
public isSatisfiedBy(email: Email): boolean {
const domain = email.getDomain()
return BlacklistedEmailSpecification.BLACKLISTED_DOMAINS.includes(domain)
}
}
/**
* Valid Email for Registration
*
* Composed specification: not blacklisted
*/
export class ValidEmailForRegistrationSpecification extends Specification<Email> {
private readonly notBlacklisted: Specification<Email>
constructor() {
super()
this.notBlacklisted = new BlacklistedEmailSpecification().not()
}
public isSatisfiedBy(email: Email): boolean {
return this.notBlacklisted.isSatisfiedBy(email)
}
}

View File

@@ -0,0 +1,79 @@
import { Specification } from "./Specification"
import { Order } from "../aggregates/Order"
import { Money } from "../value-objects/Money"
/**
* Order Can Be Cancelled Specification
*
* Business Rule: Order can be cancelled if not delivered
*/
export class OrderCanBeCancelledSpecification extends Specification<Order> {
public isSatisfiedBy(order: Order): boolean {
return !order.status.isDelivered()
}
}
/**
* Order Eligible For Discount Specification
*
* Business Rule: Orders over $100 get discount
*/
export class OrderEligibleForDiscountSpecification extends Specification<Order> {
private static readonly DISCOUNT_THRESHOLD = Money.create(100, "USD")
public isSatisfiedBy(order: Order): boolean {
const total = order.calculateTotal()
return total.isGreaterThan(OrderEligibleForDiscountSpecification.DISCOUNT_THRESHOLD)
}
}
/**
* Order Eligible For Free Shipping Specification
*
* Business Rule: Orders over $50 get free shipping
*/
export class OrderEligibleForFreeShippingSpecification extends Specification<Order> {
private static readonly FREE_SHIPPING_THRESHOLD = Money.create(50, "USD")
public isSatisfiedBy(order: Order): boolean {
const total = order.calculateTotal()
return total.isGreaterThan(
OrderEligibleForFreeShippingSpecification.FREE_SHIPPING_THRESHOLD,
)
}
}
/**
* High Value Order Specification
*
* Business Rule: Orders over $500 are high value
* (might need special handling, insurance, etc.)
*/
export class HighValueOrderSpecification extends Specification<Order> {
private static readonly HIGH_VALUE_THRESHOLD = Money.create(500, "USD")
public isSatisfiedBy(order: Order): boolean {
const total = order.calculateTotal()
return total.isGreaterThan(HighValueOrderSpecification.HIGH_VALUE_THRESHOLD)
}
}
/**
* Composed Specification: Premium Order
*
* Premium = High Value AND Eligible for Discount
*/
export class PremiumOrderSpecification extends Specification<Order> {
private readonly spec: Specification<Order>
constructor() {
super()
this.spec = new HighValueOrderSpecification().and(
new OrderEligibleForDiscountSpecification(),
)
}
public isSatisfiedBy(order: Order): boolean {
return this.spec.isSatisfiedBy(order)
}
}

View File

@@ -0,0 +1,92 @@
/**
* Specification Pattern (base class)
*
* DDD Pattern: Specification
* - Encapsulates business rules
* - Reusable predicates
* - Combinable (AND, OR, NOT)
* - Testable in isolation
*
* SOLID Principles:
* - SRP: each specification has one rule
* - OCP: extend by creating new specifications
* - LSP: all specifications are substitutable
*
* Benefits:
* - Business rules as first-class citizens
* - Reusable across use cases
* - Easy to test
* - Can be combined
*/
export abstract class Specification<T> {
/**
* Check if entity satisfies specification
*/
public abstract isSatisfiedBy(entity: T): boolean
/**
* Combine specifications with AND
*/
public and(other: Specification<T>): Specification<T> {
return new AndSpecification(this, other)
}
/**
* Combine specifications with OR
*/
public or(other: Specification<T>): Specification<T> {
return new OrSpecification(this, other)
}
/**
* Negate specification
*/
public not(): Specification<T> {
return new NotSpecification(this)
}
}
/**
* AND Specification
*/
class AndSpecification<T> extends Specification<T> {
constructor(
private readonly left: Specification<T>,
private readonly right: Specification<T>,
) {
super()
}
public isSatisfiedBy(entity: T): boolean {
return this.left.isSatisfiedBy(entity) && this.right.isSatisfiedBy(entity)
}
}
/**
* OR Specification
*/
class OrSpecification<T> extends Specification<T> {
constructor(
private readonly left: Specification<T>,
private readonly right: Specification<T>,
) {
super()
}
public isSatisfiedBy(entity: T): boolean {
return this.left.isSatisfiedBy(entity) || this.right.isSatisfiedBy(entity)
}
}
/**
* NOT Specification
*/
class NotSpecification<T> extends Specification<T> {
constructor(private readonly spec: Specification<T>) {
super()
}
public isSatisfiedBy(entity: T): boolean {
return !this.spec.isSatisfiedBy(entity)
}
}

View File

@@ -0,0 +1,62 @@
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
interface EmailProps {
readonly value: string
}
/**
* Email Value Object
*
* DDD Pattern: Value Object
* - Immutable
* - Self-validating
* - No identity
* - Equality by value
*
* Clean Code:
* - Single Responsibility: represents email
* - Meaningful name: clearly email
* - No magic values: validation rules as constants
*/
export class Email extends ValueObject<EmailProps> {
private static readonly EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
private static readonly MAX_LENGTH = 255
private constructor(props: EmailProps) {
super(props)
}
public static create(email: string): Email {
const trimmed = email.trim().toLowerCase()
if (!trimmed) {
throw new Error("Email cannot be empty")
}
if (trimmed.length > Email.MAX_LENGTH) {
throw new Error(`Email must be less than ${Email.MAX_LENGTH} characters`)
}
if (!Email.EMAIL_REGEX.test(trimmed)) {
throw new Error(`Invalid email format: ${email}`)
}
return new Email({ value: trimmed })
}
public get value(): string {
return this.props.value
}
public getDomain(): string {
return this.props.value.split("@")[1]
}
public isFromDomain(domain: string): boolean {
return this.getDomain() === domain.toLowerCase()
}
public toString(): string {
return this.props.value
}
}

View File

@@ -0,0 +1,107 @@
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
interface MoneyProps {
readonly amount: number
readonly currency: string
}
/**
* Money Value Object
*
* DDD Pattern: Value Object
* - Encapsulates amount + currency
* - Immutable
* - Rich behavior (add, subtract, compare)
*
* Prevents common bugs:
* - Adding different currencies
* - Negative amounts (when not allowed)
* - Floating point precision issues
*/
export class Money extends ValueObject<MoneyProps> {
private static readonly SUPPORTED_CURRENCIES = ["USD", "EUR", "GBP", "RUB"]
private static readonly DECIMAL_PLACES = 2
private constructor(props: MoneyProps) {
super(props)
}
public static create(amount: number, currency: string): Money {
const upperCurrency = currency.toUpperCase()
if (!Money.SUPPORTED_CURRENCIES.includes(upperCurrency)) {
throw new Error(
`Unsupported currency: ${currency}. Supported: ${Money.SUPPORTED_CURRENCIES.join(", ")}`,
)
}
if (amount < 0) {
throw new Error("Money amount cannot be negative")
}
const rounded = Math.round(amount * 100) / 100
return new Money({ amount: rounded, currency: upperCurrency })
}
public static zero(currency: string): Money {
return Money.create(0, currency)
}
public get amount(): number {
return this.props.amount
}
public get currency(): string {
return this.props.currency
}
public add(other: Money): Money {
this.ensureSameCurrency(other)
return Money.create(this.amount + other.amount, this.currency)
}
public subtract(other: Money): Money {
this.ensureSameCurrency(other)
const result = this.amount - other.amount
if (result < 0) {
throw new Error("Cannot subtract: result would be negative")
}
return Money.create(result, this.currency)
}
public multiply(multiplier: number): Money {
if (multiplier < 0) {
throw new Error("Multiplier cannot be negative")
}
return Money.create(this.amount * multiplier, this.currency)
}
public isGreaterThan(other: Money): boolean {
this.ensureSameCurrency(other)
return this.amount > other.amount
}
public isLessThan(other: Money): boolean {
this.ensureSameCurrency(other)
return this.amount < other.amount
}
public isZero(): boolean {
return this.amount === 0
}
private ensureSameCurrency(other: Money): void {
if (this.currency !== other.currency) {
throw new Error(
`Cannot operate on different currencies: ${this.currency} vs ${other.currency}`,
)
}
}
public toString(): string {
return `${this.amount.toFixed(Money.DECIMAL_PLACES)} ${this.currency}`
}
}

View File

@@ -0,0 +1,35 @@
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
import { v4 as uuidv4, validate as uuidValidate } from "uuid"
interface OrderIdProps {
readonly value: string
}
/**
* OrderId Value Object
*
* Type safety: cannot mix with UserId
*/
export class OrderId extends ValueObject<OrderIdProps> {
private constructor(props: OrderIdProps) {
super(props)
}
public static create(id?: string): OrderId {
const value = id ?? uuidv4()
if (!uuidValidate(value)) {
throw new Error(`Invalid OrderId format: ${value}`)
}
return new OrderId({ value })
}
public get value(): string {
return this.props.value
}
public toString(): string {
return this.props.value
}
}

View File

@@ -0,0 +1,92 @@
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
interface OrderStatusProps {
readonly value: string
}
/**
* OrderStatus Value Object
*
* DDD Pattern: Enum as Value Object
* - Type-safe status
* - Business logic: valid transitions
* - Self-validating
*/
export class OrderStatus extends ValueObject<OrderStatusProps> {
public static readonly PENDING = new OrderStatus({ value: "pending" })
public static readonly CONFIRMED = new OrderStatus({ value: "confirmed" })
public static readonly PAID = new OrderStatus({ value: "paid" })
public static readonly SHIPPED = new OrderStatus({ value: "shipped" })
public static readonly DELIVERED = new OrderStatus({ value: "delivered" })
public static readonly CANCELLED = new OrderStatus({ value: "cancelled" })
private static readonly VALID_STATUSES = [
"pending",
"confirmed",
"paid",
"shipped",
"delivered",
"cancelled",
]
private constructor(props: OrderStatusProps) {
super(props)
}
public static create(status: string): OrderStatus {
const lower = status.toLowerCase()
if (!OrderStatus.VALID_STATUSES.includes(lower)) {
throw new Error(
`Invalid order status: ${status}. Valid: ${OrderStatus.VALID_STATUSES.join(", ")}`,
)
}
return new OrderStatus({ value: lower })
}
public get value(): string {
return this.props.value
}
/**
* Business Rule: Valid status transitions
*/
public canTransitionTo(newStatus: OrderStatus): boolean {
const transitions: Record<string, string[]> = {
pending: ["confirmed", "cancelled"],
confirmed: ["paid", "cancelled"],
paid: ["shipped", "cancelled"],
shipped: ["delivered"],
delivered: [],
cancelled: [],
}
const allowedTransitions = transitions[this.value] ?? []
return allowedTransitions.includes(newStatus.value)
}
public isPending(): boolean {
return this.value === "pending"
}
public isConfirmed(): boolean {
return this.value === "confirmed"
}
public isCancelled(): boolean {
return this.value === "cancelled"
}
public isDelivered(): boolean {
return this.value === "delivered"
}
public isFinal(): boolean {
return this.isDelivered() || this.isCancelled()
}
public toString(): string {
return this.props.value
}
}

View File

@@ -0,0 +1,43 @@
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
import { v4 as uuidv4, validate as uuidValidate } from "uuid"
interface UserIdProps {
readonly value: string
}
/**
* UserId Value Object
*
* DDD Pattern: Identity Value Object
* - Strongly typed ID (not just string)
* - Self-validating
* - Type safety: can't mix with OrderId
*
* Benefits:
* - No accidental ID mixing: `findUser(orderId)` won't compile
* - Clear intent in code
* - Encapsulated validation
*/
export class UserId extends ValueObject<UserIdProps> {
private constructor(props: UserIdProps) {
super(props)
}
public static create(id?: string): UserId {
const value = id ?? uuidv4()
if (!uuidValidate(value)) {
throw new Error(`Invalid UserId format: ${value}`)
}
return new UserId({ value })
}
public get value(): string {
return this.props.value
}
public toString(): string {
return this.props.value
}
}

View File

@@ -0,0 +1,32 @@
import { PlaceOrder, PlaceOrderRequest } from "../../application/use-cases/PlaceOrder"
import { OrderResponseDto } from "../../application/dtos/OrderResponseDto"
/**
* Order Controller
*
* Infrastructure Layer: HTTP Controller
* - No business logic
* - Returns DTOs (not domain entities!)
* - Delegates to use cases
*/
export class OrderController {
constructor(private readonly placeOrder: PlaceOrder) {}
/**
* POST /orders
*
* ✅ Good: Returns DTO
* ✅ Good: Delegates to use case
* ✅ Good: No business logic
*/
public async placeOrder(request: PlaceOrderRequest): Promise<OrderResponseDto> {
try {
return await this.placeOrder.execute(request)
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to place order: ${error.message}`)
}
throw error
}
}
}

View File

@@ -0,0 +1,46 @@
import { CreateUser } from "../../application/use-cases/CreateUser"
import { CreateUserRequest } from "../../application/dtos/CreateUserRequest"
import { UserResponseDto } from "../../application/dtos/UserResponseDto"
/**
* User Controller
*
* Clean Architecture: Infrastructure / Presentation Layer
* - HTTP concerns (not in use case)
* - Request/Response handling
* - Error handling
* - Delegates to use cases
*
* SOLID Principles:
* - SRP: HTTP handling only
* - DIP: depends on use case abstraction
* - OCP: can add new endpoints
*
* Important:
* - NO business logic here
* - NO domain entities exposed
* - Returns DTOs only
* - Use cases do the work
*/
export class UserController {
constructor(private readonly createUser: CreateUser) {}
/**
* POST /users
*
* Clean Code:
* - Returns DTO, not domain entity
* - Delegates to use case
* - Focused method
*/
public async createUser(request: CreateUserRequest): Promise<UserResponseDto> {
try {
return await this.createUser.execute(request)
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to create user: ${error.message}`)
}
throw error
}
}
}

View File

@@ -0,0 +1,45 @@
import { IOrderRepository } from "../../domain/repositories/IOrderRepository"
import { Order } from "../../domain/aggregates/Order"
import { OrderId } from "../../domain/value-objects/OrderId"
import { UserId } from "../../domain/value-objects/UserId"
/**
* In-Memory Order Repository
*/
export class InMemoryOrderRepository implements IOrderRepository {
private readonly orders: Map<string, Order> = new Map()
public async save(order: Order): Promise<void> {
this.orders.set(order.orderId.value, order)
}
public async findById(id: OrderId): Promise<Order | null> {
return this.orders.get(id.value) ?? null
}
public async findByUserId(userId: UserId): Promise<Order[]> {
return Array.from(this.orders.values()).filter(
(order) => order.userId.value === userId.value,
)
}
public async findByStatus(status: string): Promise<Order[]> {
return Array.from(this.orders.values()).filter((order) => order.status.value === status)
}
public async findAll(): Promise<Order[]> {
return Array.from(this.orders.values())
}
public async delete(id: OrderId): Promise<void> {
this.orders.delete(id.value)
}
public async exists(id: OrderId): Promise<boolean> {
return this.orders.has(id.value)
}
public clear(): void {
this.orders.clear()
}
}

View File

@@ -0,0 +1,63 @@
import { IUserRepository } from "../../domain/repositories/IUserRepository"
import { User } from "../../domain/aggregates/User"
import { UserId } from "../../domain/value-objects/UserId"
import { Email } from "../../domain/value-objects/Email"
/**
* In-Memory User Repository
*
* DDD Pattern: Repository Implementation
* - Implements domain interface
* - Infrastructure concern
* - Can be replaced with real DB
*
* SOLID Principles:
* - DIP: implements abstraction from domain
* - SRP: manages User persistence
* - LSP: substitutable with other implementations
*
* Clean Architecture:
* - Infrastructure layer
* - Depends on domain
* - Can be swapped (in-memory, Postgres, MongoDB)
*
* Use cases:
* - Testing
* - Development
* - Prototyping
*/
export class InMemoryUserRepository implements IUserRepository {
private readonly users: Map<string, User> = new Map()
public async save(user: User): Promise<void> {
this.users.set(user.userId.value, user)
}
public async findById(id: UserId): Promise<User | null> {
return this.users.get(id.value) ?? null
}
public async findByEmail(email: Email): Promise<User | null> {
return Array.from(this.users.values()).find((user) => user.email.equals(email)) ?? null
}
public async findAll(): Promise<User[]> {
return Array.from(this.users.values())
}
public async findActive(): Promise<User[]> {
return Array.from(this.users.values()).filter((user) => user.isActive)
}
public async delete(id: UserId): Promise<void> {
this.users.delete(id.value)
}
public async exists(id: UserId): Promise<boolean> {
return this.users.has(id.value)
}
public clear(): void {
this.users.clear()
}
}