Skills

Install

$ npx ai-agents-skills add --skill clean-architecture
Domain v1.0

Clean Architecture

Organizes code into concentric layers where dependencies point inward. Business rules stay independent of frameworks, databases, and external systems.

When to Use

  • Backend projects >500 LOC with business logic
  • Need to swap databases, frameworks, or external services
  • Want business logic testable without spinning up infrastructure
  • Building multi-layered services

Don’t use for:

  • Simple CRUD APIs without real business rules
  • Scripts and utilities
  • Prototypes

Critical Patterns

✅ REQUIRED: The Dependency Rule

Dependencies must point inward only. Inner layers know nothing about outer layers.

Presentation (HTTP, CLI, GraphQL)
  Infrastructure (DB, APIs, Email) → implements Domain ports
    Application (Use Cases)        → orchestrates Domain, defines ports
      Domain (Entities)            ← NO external dependencies

✅ REQUIRED: Domain Layer — Pure Business Rules

Entities contain business rules. Zero framework or DB dependencies.

class Order {
  confirm(): void {
    if (this._status !== "pending") throw new DomainError("Can only confirm pending orders");
    this._status = "confirmed";
  }
  // No prisma, no express, no external imports
}

✅ REQUIRED: Application Layer — Ports + Use Cases

Defines interfaces (ports) for what it needs. Orchestrates domain entities.

interface IOrderRepository { save(order: Order): Promise<void>; }
interface IPaymentGateway  { charge(amount: number, token: string): Promise<PaymentResult>; }

class PlaceOrderUseCase {
  constructor(private repo: IOrderRepository, private payment: IPaymentGateway) {}
  async execute(items: OrderItemDTO[], token: string): Promise<Result<Order>> { ... }
}

✅ REQUIRED: Infrastructure Layer — Port Implementations

Implements ports with concrete tech. Only layer that knows about Prisma, Stripe, etc.

class PostgresOrderRepository implements IOrderRepository { ... }
class StripePaymentGateway     implements IPaymentGateway  { ... }

❌ NEVER: Mix Layers

❌ Domain entity calls DB:       class User { async save() { await prisma.user.update(...) } }
❌ Use case knows HTTP:          async execute(req: Request): Response { ... }
❌ Controller has business logic: app.post('/orders', (req, res) => { if (items.length === 0)... })

✅ REQUIRED: Standard Folder Structure

src/
├── domain/         → entities/, value-objects/, errors/
├── application/    → use-cases/, ports/, dto/
├── infrastructure/ → repositories/, gateways/, database/
└── presentation/   → controllers/, routes/, middleware/

Decision Tree

Has real business rules (not just CRUD)?
  → YES: Apply domain entities with business methods
  → NO: Simple folder structure is enough

Need to test business logic without DB/email/HTTP?
  → Define ports (interfaces) in application layer
  → Implement in infrastructure, inject in tests

Entity importing Prisma/Express/Stripe?
  → Move to infrastructure adapter implementing a port

Use case receiving Express Request or returning Response?
  → Move HTTP concerns to presentation layer

Frontend or backend?
  → Backend with complex logic → Clean Architecture fits well
  → Frontend SPA → Usually overkill; use state management + composition

Example

UserRegistration use case passing through all four clean architecture layers.

// Domain layer — pure business rule, no imports from outer layers
class User {
  static create(email: string, passwordHash: string): User {
    if (!email.includes("@")) throw new DomainError("Invalid email");
    return new User(crypto.randomUUID(), email, passwordHash);
  }
}

// Application layer — defines ports, orchestrates domain
interface IUserRepository { save(user: User): Promise<void>; findByEmail(email: string): Promise<User | null>; }
interface IHashService    { hash(plain: string): Promise<string>; }

class RegisterUserUseCase {
  constructor(private repo: IUserRepository, private hash: IHashService) {}
  async execute(email: string, password: string): Promise<Result<User>> {
    if (await this.repo.findByEmail(email)) return Result.fail("Email already registered");
    const hashed = await this.hash.hash(password);
    const user = User.create(email, hashed);
    await this.repo.save(user);
    return Result.ok(user);
  }
}

// Infrastructure layer — implements ports with real tech
class PostgresUserRepository implements IUserRepository { /* prisma calls */ }
class BcryptHashService    implements IHashService    { /* bcrypt calls */ }

// Presentation layer — HTTP concern only
app.post("/api/v1/users", async (req, res) => {
  const result = await registerUser.execute(req.body.email, req.body.password);
  result.isSuccess ? res.status(201).json(result.value) : res.status(400).json({ error: result.error });
});

Dependency rule satisfied: Domain ← Application ← Infrastructure / Presentation. Domain has zero external imports.


Edge Cases

Frontend Clean Architecture: Usually overkill for React/Vue apps. State management + component composition covers most needs. Apply only if the frontend has substantial business logic.

Partial adoption: Can apply just the domain + ports/adapters without full layer separation. Start with isolating external dependencies via interfaces.

Shared domain types: DTOs and value objects can cross layer boundaries, but only flowing inward. Infrastructure maps external data to domain types.

Anemic domain models: Entities with only getters/setters and all logic in services is an anti-pattern. Business rules belong in domain entities.


Resources