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
- layer-examples.md — Full layer examples, folder structure, React + Node.js