When this skill is activated, always start your first response with the 🧢 emoji.
Clean Architecture
Clean Architecture is a set of principles from Robert C. Martin for organizing software systems so that business rules are isolated from frameworks, databases, and delivery mechanisms. The core idea is the Dependency Rule: source code dependencies must always point inward, toward higher-level policies. This produces systems that are testable without UI or database, framework-independent, and resilient to change in external concerns. This skill covers the concentric layer model, component design principles, and practical boundary-crossing patterns.
When to use this skill
Trigger this skill when the user:
- Asks how to structure a new project or application
- Wants to separate business logic from framework/infrastructure code
- Needs to design use cases or application services
- Asks about dependency direction or the Dependency Rule
- Wants to refactor a monolith or tightly-coupled codebase
- Asks about component cohesion, coupling, or package organization
- Needs to cross architectural boundaries (e.g. use case to database)
- Asks about Screaming Architecture or making intent visible in structure
Do NOT trigger this skill for:
- Code-level refactoring (naming, function size, comments) - use the clean-code skill
- Infrastructure/DevOps decisions (container orchestration, CI/CD pipelines)
Key principles
-
The Dependency Rule - Source code dependencies must point inward only. Nothing in an inner circle can know anything about something in an outer circle. This includes names, functions, classes, and data formats. The inner circles are policy; the outer circles are mechanisms.
-
Screaming Architecture - Your project structure should scream its purpose. A healthcare system's top-level folders should say
patients/,appointments/,prescriptions/- notcontrollers/,models/,services/. The architecture should communicate the use cases, not the framework. -
Policy over detail - Business rules are the most important code. They change for business reasons. Frameworks, databases, and UI are details that change for technical reasons. Protect policy from detail by making detail depend on policy, never the reverse.
-
Defer decisions - A good architecture lets you delay choices about frameworks, databases, and delivery mechanisms. If you must choose a database before writing business logic, the architecture has failed.
-
Testability as a design metric - If you can't test your business rules without a database, web server, or UI, the architecture is wrong. Use cases should be testable with plain unit tests.
Core concepts
Clean Architecture organizes code into concentric layers, each with a distinct responsibility. From innermost to outermost:
Entities are enterprise-wide business rules. They encapsulate the most general, high-level rules that would exist even if there were no software system. An entity can be an object with methods or a set of data structures and functions. They are the least likely to change when something external changes.
Use Cases contain application-specific business rules. Each use case orchestrates the flow of data to and from entities, directing them to apply their enterprise-wide rules. Use cases don't know about the UI, database, or any external agency. They define input/output data structures (request/response models) at the boundary.
Interface Adapters convert data between the format most convenient for use cases and the format required by external agents (database, web, etc.). Controllers, presenters, gateways, and repositories live here. This layer contains no business logic - only translation.
Frameworks & Drivers is the outermost layer. Web frameworks, database drivers, HTTP clients, message queues. This is glue code that wires external tools to the interface adapters. Keep this layer thin.
See references/layer-patterns.md for detailed code patterns in each layer.
Common tasks
Structure a new project
Organize by domain feature, not by technical layer. Each feature module contains its own layers internally.
Before (framework-screaming):
src/
controllers/
UserController.ts
OrderController.ts
models/
User.ts
Order.ts
services/
UserService.ts
OrderService.ts
repositories/
UserRepository.ts
OrderRepository.ts
After (domain-screaming):
src/
users/
entities/User.ts
usecases/CreateUser.ts
usecases/GetUserProfile.ts
adapters/UserController.ts
adapters/UserRepository.ts
orders/
entities/Order.ts
entities/OrderItem.ts
usecases/PlaceOrder.ts
usecases/CancelOrder.ts
adapters/OrderController.ts
adapters/OrderRepository.ts
shared/
entities/Money.ts
interfaces/Repository.ts
Define a use case
Each use case is a single class/function with one public method. It accepts a request model, orchestrates entities, and returns a response model.
// usecases/PlaceOrder.ts
interface PlaceOrderRequest {
customerId: string;
items: Array<{ productId: string; quantity: number }>;
}
interface PlaceOrderResponse {
orderId: string;
total: number;
}
interface OrderGateway {
save(order: Order): Promise<void>;
}
interface ProductGateway {
findByIds(ids: string[]): Promise<Product[]>;
}
class PlaceOrder {
constructor(
private orders: OrderGateway,
private products: ProductGateway,
) {}
async execute(request: PlaceOrderRequest): Promise<PlaceOrderResponse> {
const products = await this.products.findByIds(
request.items.map((i) => i.productId),
);
const order = Order.create(request.customerId, request.items, products);
await this.orders.save(order);
return { orderId: order.id, total: order.total.amount };
}
}
Note: OrderGateway and ProductGateway are interfaces defined in the use case
layer. The database implementation lives in the adapters layer and is injected.
Cross a boundary with Dependency Inversion
When an inner layer needs to call an outer layer (e.g. use case needs to persist data), define an interface in the inner layer and implement it in the outer layer.
Use Case layer: defines OrderGateway (interface)
Adapter layer: implements PostgresOrderGateway (class)
Framework layer: wires PostgresOrderGateway into PlaceOrder via DI
// Inner: usecases/gateways/OrderGateway.ts (interface)
interface OrderGateway {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
// Outer: adapters/persistence/PostgresOrderGateway.ts (implementation)
class PostgresOrderGateway implements OrderGateway {
constructor(private db: Pool) {}
async save(order: Order): Promise<void> {
await this.db.query("INSERT INTO orders ...", [order.id, order.total]);
}
async findById(id: string): Promise<Order | null> {
const row = await this.db.query("SELECT * FROM orders WHERE id = $1", [id]);
return row ? this.toEntity(row) : null;
}
}
See references/dependency-rule.md and references/boundaries.md for more
patterns.
Design an interface adapter (Controller)
Controllers translate HTTP requests into use case request models, then translate use case responses back into HTTP responses. No business logic lives here.
// adapters/http/OrdersController.ts
class OrdersController {
constructor(private placeOrder: PlaceOrder) {}
async handlePost(req: Request, res: Response) {
const request: PlaceOrderRequest = {
customerId: req.body.customerId,
items: req.body.items,
};
const result = await this.placeOrder.execute(request);
res.status(201).json(result);
}
}
The controller knows about HTTP. The use case does not. If you switch from Express to Fastify, only this layer changes.