Tags: clean-architecture, architecture, ddd, solid, design-patterns, typescript
Level: intermediate
TL;DR: Decouple business logic from external frameworks, databases, and UI via concentric layers governed by the Dependency Rule.
Source code dependencies must point inwards only, toward higher-level policies.
Domain Entities encapsulate enterprise-wide business rules independent of any application.
Use Cases coordinate data flow to and from Entities and implement application-specific rules.
Interface Adapters convert data between Use Cases and external formats (controllers, presenters, gateways).
Frameworks and Drivers form the outermost layer (web frameworks, databases, UI, third-party devices).
The Dependency Inversion Principle keeps inner layers unaware of outer layer implementations.
Outer layers communicate inwards using abstract interfaces declared in the inner layers.
Data crossing layer boundaries uses plain Data Transfer Objects (DTOs) or basic types.
Business rules remain testable in isolation without a database, web server, or UI.
Q: What is the Dependency Rule in Clean Architecture?R: Dependencies point strictly inwards; inner circles know nothing about outer circles, interfaces, data formats, or frameworks.
Q: How do inner layers interact with external services like databases?R: The use case defines an abstraction interface (port), and the infrastructure layer implements it (adapter).
Q: What is the difference between an Entity and a Use Case?R: Entities embody core enterprise rules valid anywhere, while Use Cases orchestrate interactions for specific application scenarios.
Q: Why shouldn't Use Cases return database ORM models?R: Returning ORM models couples business layers to storage schemas, violating layer isolation and boundaries.
Leaking ORM models or database entities directly into the Domain or Use Case layers.
Allowing domain services or use cases to import external libraries, HTTP clients, or frameworks.
Creating anemic use cases that do nothing except forward calls directly to repositories.
Treating Clean Architecture as mandatory boilerplate for trivial CRUD applications with no domain rules.
Sharing mutable domain entities across API boundaries instead of explicit, isolated DTOs.
// Domain: Entity
export class Order {
constructor(public readonly id: string, private total: number) {}
applyDiscount(discount: number): void {
if (discount < 0 || discount > this.total) throw new Error("Invalid discount");
this.total -= discount;
}
}
// Application: Port (Interface)
export interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}
// Application: Use Case
export class ApplyDiscountUseCase {
constructor(private readonly orderRepo: OrderRepository) {}
async execute(orderId: string, amount: number): Promise<void> {
const order = await this.orderRepo.findById(orderId);
if (!order) throw new Error("Order not found");
order.applyDiscount(amount);
await this.orderRepo.save(order);
}
}
[ ] Boundaries between Ports & Adapters (Hexagonal) and Clean Architecture layers
[ ] Proper mapping strategies between Domain Entities, Persistence Entities, and DTOs
[ ] Designing test doubles (mocks/stubs) for Use Case ports without booting frameworks
Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin)
The Clean Architecture (Uncle Bob's original 2012 blog post)