clean-architecture
Use this skill when designing, reviewing, or refactoring software architecture following Robert C. Martin's (Uncle Bob) Clean Architecture principles. Triggers on project structure decisions, layer design, dependency management, use case modeling, boundary crossing patterns, component organization, and separating business rules from frameworks. Covers the Dependency Rule, concentric layers, component cohesion/coupling, and boundary patterns.
engineering clean-architecturearchitecturedependency-ruleuse-casesboundariescomponentsWhat is clean-architecture?
Use this skill when designing, reviewing, or refactoring software architecture following Robert C. Martin's (Uncle Bob) Clean Architecture principles. Triggers on project structure decisions, layer design, dependency management, use case modeling, boundary crossing patterns, component organization, and separating business rules from frameworks. Covers the Dependency Rule, concentric layers, component cohesion/coupling, and boundary patterns.
clean-architecture
clean-architecture is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex, and 1 more. Designing, reviewing, or refactoring software architecture following Robert C. Martin's (Uncle Bob) Clean Architecture principles.
Quick Facts
| Field | Value |
|---|---|
| Category | engineering |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex, mcp |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill clean-architecture- The clean-architecture skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
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.
Tags
clean-architecture architecture dependency-rule use-cases boundaries components
Platforms
- claude-code
- gemini-cli
- openai-codex
- mcp
Related Skills
Pair clean-architecture with these complementary skills:
Frequently Asked Questions
What is clean-architecture?
Use this skill when designing, reviewing, or refactoring software architecture following Robert C. Martin's (Uncle Bob) Clean Architecture principles. Triggers on project structure decisions, layer design, dependency management, use case modeling, boundary crossing patterns, component organization, and separating business rules from frameworks. Covers the Dependency Rule, concentric layers, component cohesion/coupling, and boundary patterns.
How do I install clean-architecture?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill clean-architecture in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support clean-architecture?
This skill works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
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.tsAfter (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.tsDefine 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.
Enforce the Dependency Rule
Use these practical enforcement strategies:
- Import linting - Configure ESLint (e.g.
eslint-plugin-boundaries) or similar tools to forbid imports from outer layers into inner layers - Package/module boundaries - In languages with module systems (Go, Java, Rust), use package visibility to enforce access
- Code review checklist - Check that entities import nothing from use cases, use cases import nothing from adapters, and adapters import nothing from frameworks directly
ALLOWED: Adapter -> UseCase -> Entity
FORBIDDEN: Entity -> UseCase, UseCase -> Adapter, Entity -> AdapterSee references/dependency-rule.md for enforcement tooling by language.
Organize components
Apply the component cohesion and coupling principles to decide what goes in the same package/module and how packages relate to each other.
Cohesion (what goes together):
- REP (Reuse/Release Equivalence) - Classes released together should be reusable together
- CCP (Common Closure) - Classes that change together should be packaged together
- CRP (Common Reuse) - Don't force consumers to depend on things they don't use
Coupling (how packages relate):
- ADP (Acyclic Dependencies) - No cycles in the package dependency graph
- SDP (Stable Dependencies) - Depend in the direction of stability
- SAP (Stable Abstractions) - Stable packages should be abstract
See references/component-principles.md for the full breakdown.
Anti-patterns / common mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Framework coupling | Letting annotations (@Entity, @Injectable) leak into entities/use cases ties business rules to a framework |
Keep entities as plain objects. Apply framework decorators only in the adapter/framework layer |
| Skipping use cases | Putting business logic in controllers makes it untestable and couples it to HTTP | Always model operations as use cases, even simple ones. They're cheap to create |
| Over-engineering small apps | Full Clean Architecture for a 3-endpoint CRUD API adds layers without benefit | Scale the architecture to the complexity. A simple app might only need 2 layers |
| Wrong dependency direction | Use cases importing from controllers, or entities depending on ORM types | Draw the dependency arrows. If any point outward, invert with an interface |
| Database-driven design | Starting with the schema and generating entities from it | Start with entities and use cases. The database schema is a detail that adapts to the domain |
| Treating layers as folders | Creating entities/, usecases/ folders but not enforcing import rules |
Folders aren't boundaries. Use linting, module visibility, or build tools to enforce the rule |
| Premature microservices | Splitting into services before understanding domain boundaries | Start as a well-structured monolith. Extract services along proven component boundaries |
Gotchas
Framework annotations leaking into entities - JPA's
@Entity, Spring's@Component, or NestJS's@Injectableplaced on domain entities ties your core business objects to a framework. When the framework upgrades or changes, entities must change too - exactly what Clean Architecture prevents. Keep entities as plain classes; apply framework annotations only in the adapter/framework layer.Use case explosion without value - Every CRUD operation does not need a dedicated use case class. A
GetUserByIduse case that does nothing except calluserRepository.findById()adds a layer of indirection with no benefit. Apply use cases for operations that involve multiple entities, enforce business rules, or have meaningful orchestration logic.DTOs at the wrong boundary - Data Transfer Objects exist to prevent entity objects from crossing layer boundaries. A common mistake is passing the entity directly from the use case to the controller (coupling the HTTP response shape to the domain model). Always define explicit request/response models at each boundary.
Circular imports through shared folders - A
shared/orcommon/directory that grows to contain business logic creates a hidden coupling layer. Entities start importing fromshared/utilswhich imports from other features. Auditshared/regularly - it should contain only pure utilities with zero business logic.Testing the framework instead of the use case - Integration tests that spin up a web server to test a use case are testing the framework wiring, not the business logic. Use cases should be unit-testable with constructor injection and mock gateways. If you can't test a use case without HTTP, the architecture has leaked.
References
For detailed content on specific topics, read the relevant file from references/:
references/dependency-rule.md- The Dependency Rule, enforcement strategies, and tooling by languagereferences/component-principles.md- Cohesion (REP, CCP, CRP) and Coupling (ADP, SDP, SAP) with examplesreferences/layer-patterns.md- Detailed code patterns for each architectural layerreferences/boundaries.md- Boundary crossing strategies, humble objects, DTOs, partial boundaries
Only load a references file if the current task requires deep detail on that topic.
References
boundaries.md
Boundaries
Boundaries are the lines drawn between software components where the Dependency Rule is enforced. Crossing a boundary always involves an inner layer calling or being called by an outer layer. The challenge is doing this without violating the dependency direction.
Full boundary
A full boundary uses complete Dependency Inversion: an interface on the inner side, an implementation on the outer side, injected at the composition root.
Inner layer: Port (interface)
Outer layer: Adapter (implementation)
Composition: Wires adapter into portInput boundary (outside calls in):
// Use case defines the input port
interface CreateOrderUseCase {
execute(input: CreateOrderInput): Promise<CreateOrderOutput>;
}
// Use case implementation
class CreateOrder implements CreateOrderUseCase {
execute(input: CreateOrderInput): Promise<CreateOrderOutput> { ... }
}
// Controller calls through the interface
class OrdersController {
constructor(private createOrder: CreateOrderUseCase) {}
}Output boundary (inside calls out):
// Use case defines the output port (gateway)
interface OrderGateway {
save(order: Order): Promise<void>;
}
// Adapter implements it
class PostgresOrderGateway implements OrderGateway {
save(order: Order): Promise<void> { ... }
}Cost: Two extra types (interface + implementation) per boundary crossing. Use when the boundary is architecturally significant and you need to swap implementations (test doubles, different databases, different delivery mechanisms).
Partial boundary
When a full boundary feels like over-engineering but you want to preserve the option for later, use a partial boundary.
Strategy 1: Interface with single implementation
Define the interface but have only one concrete class. Skip the DI framework - just instantiate directly. You can add the DI wiring later when a second implementation appears.
// Interface exists for documentation and future flexibility
interface NotificationSender {
send(to: string, message: string): Promise<void>;
}
// Only one implementation right now
class EmailNotificationSender implements NotificationSender {
send(to: string, message: string): Promise<void> { ... }
}
// Directly instantiated (no DI container needed)
const sender = new EmailNotificationSender();
const useCase = new ProcessOrder(sender);Strategy 2: Facade pattern
Put a simple facade in front of a complex subsystem. The facade is the boundary - it presents a simplified interface that hides the subsystem's complexity.
// Facade hides the payment processing complexity
class PaymentFacade {
processPayment(amount: Money, method: PaymentMethod): PaymentResult {
// Internally uses Stripe SDK, retry logic, logging, etc.
// Callers see only this simple interface
}
}Strategy 3: Same-package boundary
Keep the interface and implementation in the same package but use the interface as the public API. This doesn't enforce the boundary at build time but signals intent.
Cost of partial boundaries: They tend to degrade over time if not actively maintained. The implementation starts leaking through the boundary because "it's right there." Use code reviews to keep the boundary clean.
Humble Object Pattern
The Humble Object pattern separates hard-to-test behavior from easy-to-test behavior by placing them in different classes. The hard-to-test class (the humble object) is stripped down to the bare minimum.
The pattern:
TestablePresenter <-- contains all the logic, easy to test
HumbleView <-- just renders what the presenter tells it toExample: Presenter + View
// Presenter - testable, contains display logic
class OrderPresenter {
present(output: CreateOrderOutput): OrderViewModel {
return {
title: `Order #${output.orderId}`,
totalDisplay: `$${output.total.toFixed(2)}`,
itemSummary: `${output.itemCount} item${output.itemCount !== 1 ? "s" : ""}`,
isLargeOrder: output.total > 1000,
};
}
}
// View - humble, just renders the view model
class OrderView {
render(viewModel: OrderViewModel): string {
return `<div class="${viewModel.isLargeOrder ? 'highlight' : ''}">
<h2>${viewModel.title}</h2>
<p>${viewModel.totalDisplay} - ${viewModel.itemSummary}</p>
</div>`;
}
}The presenter is a pure function - no DOM, no HTTP, no framework. Easy to test. The view is a humble object - it just takes the view model and renders. So simple it barely needs testing.
Where humble objects appear
| Boundary | Testable side | Humble side |
|---|---|---|
| UI | Presenter | View / Component |
| Database | Gateway interface | ORM mapping code |
| HTTP | Controller logic | Framework routing |
| External service | Service interface | API client wrapper |
Data Transfer Objects (DTOs)
DTOs carry data across boundaries. They are simple data structures with no behavior - just fields.
Rules for DTOs
- Each boundary has its own DTOs. Don't reuse the same DTO across multiple boundaries.
- DTOs are not entities. Entities have behavior and enforce invariants. DTOs are flat data bags.
- Map at the boundary. The adapter is responsible for converting between entity and DTO.
// Use case boundary DTO
interface CreateOrderInput {
customerId: string;
items: Array<{ productId: string; quantity: number }>;
}
// HTTP boundary DTO (may have different field names, validation)
interface CreateOrderHttpBody {
customer_id: string; // snake_case from API convention
line_items: Array<{ sku: string; qty: number }>;
}
// Database boundary DTO
interface OrderRow {
id: string;
customer_id: string;
status: string;
total_cents: number; // stored as cents, not dollars
created_at: Date;
}Each DTO is shaped for its boundary's needs. The controller maps HTTP body to use case input. The repository maps database rows to entities. Never let one boundary's DTO leak into another.
When to draw boundaries
Not every boundary needs full enforcement from day one. Use this decision guide:
| Signal | Boundary type |
|---|---|
| Different teams own each side | Full boundary (enforced at build time) |
| Will definitely swap implementations (test vs prod DB) | Full boundary |
| Might swap later, one implementation now | Partial boundary (interface + single impl) |
| Conceptual separation only, same developer | Code organization (folders, naming) |
| Small app, simple CRUD | Maybe no internal boundaries at all |
Start with fewer boundaries and add them when the cost of not having them exceeds the cost of maintaining them. A boundary that never gets crossed by a second implementation is pure overhead.
Boundary violation symptoms
Watch for these signs that a boundary is being violated:
- Import from wrong direction - An entity importing a use case type
- Shared mutable state - Two sides of a boundary modifying the same object
- Leaking types - Database column types appearing in use case code
- Test difficulty - Needing a real database to test business logic
- Cascade changes - Changing a database column requires updating a controller
component-principles.md
Component Principles
Components are the units of deployment - the smallest things that can be deployed as part of a system. In different languages these are jars, gems, npm packages, DLLs, or Go modules. The component principles guide what goes into each component and how components relate to each other.
Component Cohesion (what goes together)
These three principles are in tension with each other. You balance them based on project maturity.
REP - Reuse/Release Equivalence Principle
The granule of reuse is the granule of release.
Classes and modules that are grouped into a component should be releasable together. If they're reused together, they should be versioned together.
Practical meaning: Don't put unrelated classes in the same package just because they're "utilities." If someone depends on your component for class A, they shouldn't be forced to accept changes to unrelated class B.
Violation: A utils package containing string helpers, date formatters, HTTP
clients, and logging wrappers. A change to the HTTP client forces a new release
that string-helper consumers must adopt.
Fix: Split into string-utils, http-client, logger packages that can
be versioned independently.
CCP - Common Closure Principle
Gather into components those classes that change for the same reasons and at the same times. Separate into different components those classes that change at different times and for different reasons.
This is SRP for components. When a requirement changes, you want the change concentrated in as few components as possible.
Practical meaning: If two classes always change together (same pull request, same sprint), they belong in the same component. If they change independently, separate them.
Example: OrderValidator and OrderPricer both change when business rules
about orders change. Put them in the same order-rules component.
OrderValidator and EmailFormatter change for different reasons. Separate them.
CRP - Common Reuse Principle
Don't force users of a component to depend on things they don't need.
When someone depends on your component, they depend on the whole thing. Every class in that component is a potential source of change that affects all consumers.
Practical meaning: If only 2 of 10 classes in a component are used by a consumer, the other 8 are unnecessary coupling. Split the component so consumers only depend on what they actually use.
This is ISP for components.
Balancing the three
REP
/ \
/ \
CCP -------- CRP- Early in development: Favor CCP (convenience of co-location). You're changing things rapidly and want minimal components to modify.
- As the system matures: Shift toward REP and CRP (independent releases, minimal coupling). Consumers need stable, focused components.
- The tension is permanent. You're always choosing which principle to relax. Acknowledge the trade-off explicitly.
Component Coupling (how they relate)
ADP - Acyclic Dependencies Principle
Allow no cycles in the component dependency graph.
If component A depends on B, and B depends on C, and C depends on A, you have a cycle. Cycles mean:
- You can't build/test components independently
- Changes cascade unpredictably
- Release order becomes impossible to determine
Detection: Draw the dependency graph. If you can't topologically sort it, there's a cycle.
Breaking cycles - two strategies:
- Dependency Inversion - If A depends on B and B depends on A, extract an interface. Make B depend on an interface that A implements.
Before: A -> B -> A (cycle)
After: A -> B -> InterfaceX (interface, owned by B)
A implements InterfaceX- Extract a new component - Move the shared dependency into a new component that both A and B depend on.
Before: A -> B -> A (cycle)
After: A -> C
B -> CSDP - Stable Dependencies Principle
Depend in the direction of stability.
A component that is depended on by many other components is stable - it has many reasons not to change. A component that depends on many others is unstable - it has many reasons to change.
Depend on the stable things. Don't make a stable component depend on a volatile one.
Stability metric (I):
I = Fan-out / (Fan-in + Fan-out)
Fan-in: number of components that depend on this one
Fan-out: number of components this one depends on
I = 0: maximally stable (everyone depends on it, it depends on nothing)
I = 1: maximally unstable (it depends on everything, nothing depends on it)The Dependency Rule naturally aligns with SDP: entities (I=0, maximally stable) are depended on by use cases, which are depended on by adapters, which are depended on by frameworks (I=1, maximally unstable).
SAP - Stable Abstractions Principle
A component should be as abstract as it is stable.
Stable components (low I) should be abstract - composed mainly of interfaces and abstract classes. This makes them open for extension even though they're hard to change.
Unstable components (high I) should be concrete - they're easy to change, so they don't need the protection of abstraction.
Abstractness metric (A):
A = Number of abstract classes and interfaces / Total number of classes
A = 0: fully concrete
A = 1: fully abstractThe Main Sequence: Plot components on an I vs A graph. The ideal is the line from (0, 1) to (1, 0) - stable things are abstract, unstable things are concrete. Distance from this line indicates design problems:
- Zone of Pain (0, 0) - Stable and concrete. Hard to change but depended on by everything. Example: a concrete utility library everyone imports.
- Zone of Uselessness (1, 1) - Unstable and abstract. Interfaces nobody implements. Dead abstraction.
Practical component organization
For a typical Clean Architecture project:
| Component | Stability (I) | Abstractness (A) | Contains |
|---|---|---|---|
domain |
~0 (stable) | High (interfaces + entities) | Entities, value objects, domain interfaces |
application |
~0.3 | Medium (use case interfaces) | Use cases, input/output ports |
infrastructure |
~0.7 | Low (concrete) | Database, HTTP, messaging implementations |
main / bootstrap |
1.0 (unstable) | 0 (concrete) | Composition root, wiring, config |
This naturally follows the Main Sequence and aligns with the Dependency Rule.
dependency-rule.md
The Dependency Rule
The Dependency Rule is the single most important rule of Clean Architecture:
Source code dependencies must only point inward.
Nothing in an inner circle can know anything at all about something in an outer circle. This includes names, functions, classes, data types, or any other named software entity.
Why it matters
When inner layers depend on outer layers, changes to frameworks, databases, or UI cascade into business rules. This creates:
- Fragility - A database schema change breaks business logic
- Rigidity - Can't change the web framework without rewriting use cases
- Untestability - Can't test business rules without standing up infrastructure
The Dependency Rule prevents all three by ensuring the most important code (policy) is protected from the least important code (mechanism).
The direction of dependencies
Frameworks & Drivers -> Interface Adapters -> Use Cases -> Entities
(outer) (inner)Each arrow means "depends on" / "knows about" / "imports from."
Allowed:
- A controller (adapter) imports a use case
- A use case imports an entity
- A repository implementation (adapter) imports a repository interface (use case)
Forbidden:
- An entity imports a use case
- A use case imports a controller
- A use case imports a concrete database class
- An entity imports an ORM decorator
Crossing boundaries inward (easy)
When an outer layer needs something from an inner layer, it simply imports it. This follows the natural dependency direction.
// Adapter imports Use Case - allowed
import { PlaceOrder } from "../usecases/PlaceOrder";
class OrdersController {
constructor(private placeOrder: PlaceOrder) {}
}Crossing boundaries outward (requires inversion)
When an inner layer needs to call an outer layer (e.g. a use case needs to save to a database), you must invert the dependency:
- Define an interface in the inner layer
- Create an implementation in the outer layer
- Inject the implementation at the composition root
Use Case layer: OrderGateway (interface) <-- dependency points inward
Adapter layer: PostgresOrderGateway (class) -- implements the interface
Composition root: new PlaceOrder(new PostgresOrderGateway(db))This way the use case depends on an abstraction it owns, not on a concrete database class.
The Composition Root
The composition root is the single place (usually in main or the framework's
bootstrap) where all concrete dependencies are wired together. It's the only place
that knows about all layers.
// main.ts (composition root - outermost layer)
const db = new Pool(config);
const orderGateway = new PostgresOrderGateway(db);
const productGateway = new PostgresProductGateway(db);
const placeOrder = new PlaceOrder(orderGateway, productGateway);
const controller = new OrdersController(placeOrder);
app.post("/orders", (req, res) => controller.handlePost(req, res));Data crossing boundaries
Data that crosses a boundary should be in the form most convenient for the inner layer. Never pass database rows, ORM entities, or HTTP request objects across a boundary.
Request/Response models (DTOs):
// Defined in the use case layer - simple data structures
interface PlaceOrderRequest {
customerId: string;
items: Array<{ productId: string; quantity: number }>;
}
interface PlaceOrderResponse {
orderId: string;
total: number;
}The controller converts HTTP data into PlaceOrderRequest. The use case returns
PlaceOrderResponse. The controller converts that into an HTTP response. Each
layer owns its own data format.
Never pass entities out of the use case layer. Return a response model instead. This prevents outer layers from calling entity methods or depending on entity structure.
Enforcement strategies by language
TypeScript / JavaScript
eslint-plugin-boundaries- Define layer zones and allowed import directionseslint-plugin-importwithno-restricted-imports- Block specific import paths- Path aliases in
tsconfig.json- Make violations visually obvious
// eslint config example
{
"rules": {
"boundaries/element-types": [2, {
"default": "disallow",
"rules": [
{ "from": "entities", "allow": [] },
{ "from": "usecases", "allow": ["entities"] },
{ "from": "adapters", "allow": ["usecases", "entities"] },
{ "from": "frameworks", "allow": ["adapters", "usecases", "entities"] }
]
}]
}
}Java / Kotlin
- ArchUnit - Write architecture tests that verify dependency rules at build time
- Java modules (JPMS) - Use
module-info.javato control exports/requires - Package-private visibility - Don't make classes public unless they need to cross boundaries
// ArchUnit test
@Test
void entities_should_not_depend_on_usecases() {
noClasses().that().resideInAPackage("..entities..")
.should().dependOnClassesThat().resideInAPackage("..usecases..")
.check(importedClasses);
}Go
- Internal packages (
internal/) prevent external imports by convention - Package-level visibility (unexported types) enforces boundaries naturally
go-cleanarchlinter checks dependency direction
Python
import-linter- Define contracts that forbid certain import paths- Separate packages with
__init__.pycontrolling exports
Common violations and fixes
| Violation | Example | Fix |
|---|---|---|
| Entity imports ORM | from sqlalchemy import Column in entity |
Keep entities as plain classes. Map to ORM in the adapter |
| Use case imports HTTP | from express import Request in use case |
Define a request DTO. Controller converts HTTP to DTO |
| Use case imports concrete DB | import { PrismaClient } in use case |
Define a gateway interface. Inject Prisma implementation |
| Entity knows about JSON | toJSON() method on entity |
Put serialization in the adapter layer |
| Framework annotations on entities | @Entity, @Column decorators |
Use separate ORM models and map to/from entities |
layer-patterns.md
Layer Patterns
Detailed code patterns for each layer of Clean Architecture. Each section shows what belongs in the layer, what doesn't, and concrete implementation patterns.
Entities Layer (innermost)
What belongs here
- Business objects with enterprise-wide rules
- Value objects (immutable, equality by value)
- Domain events
- Enums representing business concepts
What does NOT belong here
- Framework annotations (
@Entity,@Column,@Injectable) - Database concerns (IDs from auto-increment, timestamps from ORM)
- Serialization logic (
toJSON,fromJSON) - Validation that depends on external state
Patterns
Entity with business rules:
class Order {
private items: OrderItem[] = [];
private status: OrderStatus = OrderStatus.DRAFT;
addItem(product: Product, quantity: number): void {
if (this.status !== OrderStatus.DRAFT) {
throw new OrderAlreadySubmittedError(this.id);
}
if (quantity <= 0) {
throw new InvalidQuantityError(quantity);
}
const existing = this.items.find((i) => i.productId === product.id);
if (existing) {
existing.increaseQuantity(quantity);
} else {
this.items.push(OrderItem.create(product, quantity));
}
}
submit(): void {
if (this.items.length === 0) {
throw new EmptyOrderError(this.id);
}
this.status = OrderStatus.SUBMITTED;
}
get total(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.subtotal),
Money.zero("USD"),
);
}
}Value object:
class Money {
private constructor(
readonly amount: number,
readonly currency: string,
) {
if (amount < 0) throw new NegativeAmountError(amount);
}
static of(amount: number, currency: string): Money {
return new Money(amount, currency);
}
static zero(currency: string): Money {
return new Money(0, currency);
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new CurrencyMismatchError(this.currency, other.currency);
}
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
}Key properties of value objects:
- Immutable (methods return new instances)
- Equality by value, not reference
- Self-validating (constructor enforces invariants)
- No identity (no ID field)
Use Cases Layer
What belongs here
- Application-specific business rules (one class per use case)
- Input/output port interfaces (request/response models)
- Gateway interfaces (abstractions for external dependencies)
- Application-level validation (does this user have permission?)
What does NOT belong here
- Framework types (HTTP requests, database connections)
- Presentation logic (formatting, HTML, JSON structure)
- Infrastructure details (SQL queries, API calls)
Patterns
Use case with input/output ports:
// Input port (what the use case accepts)
interface CreateOrderInput {
customerId: string;
items: Array<{ productId: string; quantity: number }>;
}
// Output port (what the use case returns)
interface CreateOrderOutput {
orderId: string;
total: number;
itemCount: number;
}
// Gateway interfaces (what the use case needs from the outside)
interface CustomerGateway {
findById(id: string): Promise<Customer | null>;
}
interface OrderGateway {
save(order: Order): Promise<void>;
nextId(): Promise<string>;
}
interface ProductGateway {
findByIds(ids: string[]): Promise<Product[]>;
}
// The use case itself
class CreateOrder {
constructor(
private customers: CustomerGateway,
private orders: OrderGateway,
private products: ProductGateway,
) {}
async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
const customer = await this.customers.findById(input.customerId);
if (!customer) throw new CustomerNotFoundError(input.customerId);
const productIds = input.items.map((i) => i.productId);
const products = await this.products.findByIds(productIds);
const orderId = await this.orders.nextId();
const order = Order.create(orderId, customer);
for (const item of input.items) {
const product = products.find((p) => p.id === item.productId);
if (!product) throw new ProductNotFoundError(item.productId);
order.addItem(product, item.quantity);
}
order.submit();
await this.orders.save(order);
return {
orderId: order.id,
total: order.total.amount,
itemCount: input.items.length,
};
}
}Key pattern: The use case defines the gateway interfaces it needs. It does not import concrete implementations. The implementations are injected.
Interface Adapters Layer
What belongs here
- Controllers (convert external input to use case input)
- Presenters (convert use case output to external output)
- Repository implementations (convert between entities and database)
- API clients (convert between entities and external service formats)
- Mappers/translators between data formats
What does NOT belong here
- Business rules or domain logic
- Direct framework configuration
Patterns
Controller (HTTP to use case):
class OrdersController {
constructor(private createOrder: CreateOrder) {}
async handleCreate(req: Request, res: Response): Promise<void> {
const input: CreateOrderInput = {
customerId: req.body.customerId,
items: req.body.items,
};
const output = await this.createOrder.execute(input);
res.status(201).json({
id: output.orderId,
total: `$${output.total.toFixed(2)}`,
items: output.itemCount,
});
}
}Repository implementation (entity to database):
class PostgresOrderGateway implements OrderGateway {
constructor(private db: Pool) {}
async save(order: Order): Promise<void> {
await this.db.query(
"INSERT INTO orders (id, customer_id, status, total) VALUES ($1, $2, $3, $4)",
[order.id, order.customerId, order.status, order.total.amount],
);
for (const item of order.items) {
await this.db.query(
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",
[order.id, item.productId, item.quantity, item.price.amount],
);
}
}
async findById(id: string): Promise<Order | null> {
const row = await this.db.query("SELECT * FROM orders WHERE id = $1", [id]);
if (!row) return null;
return this.toEntity(row);
}
private toEntity(row: any): Order {
// Map database row back to domain entity
return Order.reconstitute(row.id, row.customer_id, row.status, ...);
}
}Key pattern: The mapper (toEntity) lives in the adapter. The entity has a
reconstitute factory method for rebuilding from persisted state, separate from
the create method used for new instances.
Frameworks & Drivers Layer (outermost)
What belongs here
- Framework configuration (Express routes, Spring beans, Django URLs)
- Database connection setup
- The composition root (dependency injection wiring)
- Entry point (
main)
Patterns
Composition root:
// main.ts - wires everything together
import { Pool } from "pg";
import { createApp } from "express";
// Infrastructure
const db = new Pool({ connectionString: process.env.DATABASE_URL });
// Gateways (adapters implementing use case interfaces)
const orderGateway = new PostgresOrderGateway(db);
const customerGateway = new PostgresCustomerGateway(db);
const productGateway = new PostgresProductGateway(db);
// Use cases (injected with gateways)
const createOrder = new CreateOrder(customerGateway, orderGateway, productGateway);
const cancelOrder = new CancelOrder(orderGateway);
// Controllers (injected with use cases)
const ordersController = new OrdersController(createOrder);
const cancellationController = new CancellationController(cancelOrder);
// Routes (framework glue)
const app = createApp();
app.post("/orders", (req, res) => ordersController.handleCreate(req, res));
app.post("/orders/:id/cancel", (req, res) => cancellationController.handle(req, res));
app.listen(3000);This is the only file that knows about every layer. It's the outermost circle, maximally unstable (I=1), and that's fine - it changes whenever anything changes.
Layer boundary rules summary
| From | Can import from | Cannot import from |
|---|---|---|
| Entities | Nothing (only language stdlib) | Use cases, adapters, frameworks |
| Use Cases | Entities | Adapters, frameworks |
| Adapters | Use cases, entities | Frameworks (except via injection) |
| Frameworks | Adapters, use cases, entities | - (can see everything) |
Frequently Asked Questions
What is clean-architecture?
Use this skill when designing, reviewing, or refactoring software architecture following Robert C. Martin's (Uncle Bob) Clean Architecture principles. Triggers on project structure decisions, layer design, dependency management, use case modeling, boundary crossing patterns, component organization, and separating business rules from frameworks. Covers the Dependency Rule, concentric layers, component cohesion/coupling, and boundary patterns.
How do I install clean-architecture?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill clean-architecture in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support clean-architecture?
clean-architecture works with claude-code, gemini-cli, openai-codex, mcp. Install it once and use it across any supported AI coding agent.