joabgonzalez

clean-architecture

"Layer-based architecture with inward dependency rule. Trigger: When separating business logic from frameworks, databases, and external services."

joabgonzalez 5 Updated 3mo ago

Resources

1
GitHub

Install

npx skillscat add joabgonzalez/ai-agents-skills/clean-architecture

Install via the SkillsCat registry.

SKILL.md

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