The dependency rule and concentric circles skill

The Dependency Rule is the single most important concept in Clean Architecture.

by wondelai·MIT license·★ 2,235 Stars on the repo·GitHub ↗

Use now

Files of The dependency rule and concentric circles

wondelai/main1 file
dependency-rule.md
Show the full text246 lines

The Dependency Rule and Concentric Circles

The Dependency Rule is the single most important concept in Clean Architecture. It states that source code dependencies can only point inward. Nothing in an inner circle can know anything at all about something in an outer circle. This includes names -- functions, classes, variables, data formats, or any other named software entity declared in an outer circle must not be mentioned by code in an inner circle.

This reference covers the concentric circles model, how data crosses boundaries, why direction matters, how frameworks violate the rule, and how to keep the inner circle pure.

The Concentric Circles

Clean Architecture organizes code into concentric circles, each representing a different level of abstraction and policy. The innermost circles contain the highest-level, most general policies. The outermost circles contain the lowest-level, most concrete details.

Circle 1: Entities (Innermost)

Entities encapsulate enterprise-wide business rules. These are the most general, most stable rules in the system. They are the least likely to change when something external changes -- a page navigation change, a security policy change, or a database migration should not affect entities.

Characteristics of well-designed entities:

  • They can be simple objects with methods, or they can be a set of data structures and functions
  • They encapsulate the most critical business rules
  • They have no dependency on anything in the outer circles
  • They would exist even if no software system existed (the rules are inherent to the business)
  • They are the most reusable elements across different applications in the enterprise

Example:

class LoanApplication:
    def __init__(self, applicant_income: float, requested_amount: float, credit_score: int):
        self.applicant_income = applicant_income
        self.requested_amount = requested_amount
        self.credit_score = credit_score

    def debt_to_income_ratio(self) -> float:
        return self.requested_amount / (self.applicant_income * 12)

    def is_creditworthy(self) -> bool:
        return self.credit_score >= 680 and self.debt_to_income_ratio() < 0.43

This entity knows nothing about databases, HTTP, or frameworks. It encapsulates the business rule that determines creditworthiness.

Circle 2: Use Cases

Use Cases contain application-specific business rules. They orchestrate the flow of data to and from entities and direct those entities to use their enterprise-wide business rules to achieve the goals of the use case.

Characteristics:

  • They define and implement input and output port interfaces
  • They manipulate entities to achieve application goals
  • Changes to use cases do not affect entities
  • Changes to external layers (database, UI) do not affect use cases
Circle 3: Interface Adapters

This circle contains adapters that convert data between the format most convenient for the use cases and entities and the format most convenient for some external agency such as the database or the web.

Contains:

  • Controllers (translate inbound requests to use case input)
  • Presenters (translate use case output to external format)
  • Gateways (implement repository interfaces using specific technologies)
Circle 4: Frameworks and Drivers (Outermost)

The outermost layer is composed of frameworks and tools -- the database, the web framework, the messaging system. This is where all the details go. The web is a detail. The database is a detail. We keep these things on the outside where they can do little harm.

Contains:

  • Web framework (Express, Spring, Django, Rails)
  • Database engine and ORM
  • External service clients
  • Device drivers and I/O

The Direction of Dependencies

The arrow of dependency is not the same as the arrow of control flow. Control flow can go in any direction across a boundary. Source code dependencies, however, must always point inward.

How Control Flow Opposes Dependency Direction

Consider this scenario: a controller needs to call a use case, and the use case needs to call a presenter. The control flows outward (from use case to presenter), but the dependency must point inward (presenter depends on use case, not the other way around).

The mechanism is Dependency Inversion:

Controller --> [Use Case Input Port] <-- Use Case Interactor --> [Use Case Output Port] <-- Presenter

The Use Case defines both the Input Port (which the Controller calls) and the Output Port (which the Presenter implements). The Use Case never knows about the Controller or the Presenter directly. It only knows about the interfaces it defines.

# Defined in the Use Case circle
class PlaceOrderOutputPort(ABC):
    @abstractmethod
    def present_success(self, response: OrderResponse) -> None:
        pass

    @abstractmethod
    def present_failure(self, error: str) -> None:
        pass

# Defined in the Use Case circle
class PlaceOrderInteractor:
    def __init__(self, order_repo: OrderRepository, presenter: PlaceOrderOutputPort):
        self.order_repo = order_repo
        self.presenter = presenter

    def execute(self, request: PlaceOrderRequest) -> None:
        order = Order.create(request.items, request.customer_id)
        self.order_repo.save(order)
        self.presenter.present_success(OrderResponse(order.id, order.total))

# Defined in the Adapters circle -- implements the Use Case's interface
class JsonOrderPresenter(PlaceOrderOutputPort):
    def present_success(self, response: OrderResponse) -> None:
        self.view_model = {"order_id": response.id, "total": str(response.total)}

    def present_failure(self, error: str) -> None:
        self.view_model = {"error": error}

The Interactor defines PlaceOrderOutputPort. The JsonOrderPresenter in the outer circle implements it. The dependency points inward even though control flows outward.

Data Crossing Boundaries

When data crosses a boundary, it is always in the form that is most convenient for the inner circle. The outer circle must adapt its data into the form expected by the inner circle.

Principle: Inner Circle Dictates Data Format

Wrong -- outer circle format leaking inward:

# Use Case receives a Django request object (framework dependency)
class CreateUserInteractor:
    def execute(self, request: HttpRequest):  # VIOLATION: knows about Django
        data = json.loads(request.body)
        user = User(name=data['name'])

Right -- inner circle defines its own data structure:

# Use Case defines its own request model
@dataclass
class CreateUserRequest:
    name: str
    email: str

class CreateUserInteractor:
    def execute(self, request: CreateUserRequest):  # Pure data structure
        user = User(name=request.name, email=request.email)

The Controller in the outer circle is responsible for translating the HTTP request into the CreateUserRequest.

Crossing Data Patterns
Pattern When to Use Example
Request/Response DTOs Standard use case boundaries CreateOrderRequest and CreateOrderResponse as plain data classes
Primitives Simple boundaries with few parameters get_user(user_id: str) -> UserResponse
Domain events Communicating between bounded contexts OrderPlaced(order_id, timestamp) emitted by inner circle
Data maps (dicts) Crossing boundaries where type safety is less critical Acceptable in dynamic languages; prefer typed DTOs in static ones
What Must Not Cross Boundaries
  • ORM entities or database rows: These are outer circle artifacts. Never pass an ActiveRecord model into a Use Case.
  • Framework request/response objects: HttpRequest, HttpResponse, Request, Response -- all belong in the outer circle.
  • Third-party library types: If your Use Case accepts an AwsS3Object, you've coupled business logic to AWS.

How Frameworks Violate the Dependency Rule

Frameworks want to be the center of your universe. They ask you to subclass their base classes, decorate your code with their annotations, and structure your project according to their conventions. Every such demand is a dependency pointing outward-to-inward -- a violation.

Common Framework Violations
Framework Pattern Violation Fix
ORM annotations on entities Entity depends on database framework Separate domain entity from ORM model; map between them
Controller base classes Business logic inherits framework code Use composition: controller holds a reference to the interactor
Framework-specific return types Use Case returns ResponseEntity or JsonResponse Return plain DTOs; let the adapter format the response
Dependency injection via framework Inner circle annotated with @Inject, @Autowired Use constructor injection with plain interfaces; wire in Main
Validation annotations Business validation tied to framework Validate in the use case using plain code or a domain validator
Keeping Frameworks at Arm's Length

The key insight is to treat the framework as a plugin, not as your architecture:

  1. Don't derive from framework base classes in your business logic. If the framework requires inheritance, create a thin adapter that inherits from the framework class and delegates to your clean inner code.

  2. Don't scatter framework annotations throughout your domain. If you must use annotations for ORM mapping, do so on a separate persistence model that maps to and from your domain entity.

  3. Structure your project by business capability, not by framework convention. Instead of controllers/, models/, services/ (framework-driven), use orders/, payments/, shipping/ (domain-driven), each with its own layers inside.

Keeping the Inner Circle Pure

The inner circle is the most valuable part of the system because it contains the rules that make the business money. Protecting it from contamination requires vigilance.

Purity Checklist
  • No imports from outer circles: Grep your entity and use case code for imports of framework, database, or infrastructure packages. There should be none.
  • No I/O: Inner circle code never reads from a file, queries a database, or makes an HTTP call directly. It calls an interface, and the outer circle provides the implementation.
  • No global state or singletons that come from outer circles: If a use case accesses Settings.DATABASE_URL, it depends on infrastructure.
  • No concurrency primitives from the framework: Threads, async runtime, and event loops are outer circle concerns. Use cases should be synchronous-looking; the adapter handles async mechanics.
  • Testable in isolation: If you cannot instantiate a use case with mock implementations and run it without starting any server, database, or framework, the inner circle is not pure.
Enforcement Strategies
Strategy How It Works Tools
Architecture tests Automated tests that verify import rules ArchUnit (Java), Dependency Cruiser (JS/TS), import-linter (Python)
Module boundaries Language-level visibility (packages, modules) Java modules, Go internal packages, Rust pub(crate)
Build system separation Inner and outer circles are separate build targets Separate Gradle modules, npm packages, or Python packages
Code review rules Manual review for dependency direction violations PR checklist: "Do any new imports in the domain cross outward?"
The Four-Step Inversion Process

When you discover an outward dependency in an inner circle:

  1. Identify the dependency: What concrete outer-circle class is being referenced?
  2. Define an interface in the inner circle that describes what the inner circle needs (not what the outer circle provides).
  3. Move the concrete implementation to the outer circle, implementing the inner circle's interface.
  4. Wire the dependency in Main (the composition root), injecting the concrete implementation into the inner circle at startup.

This process always works. It may feel like ceremony, but it's the mechanism that keeps the most valuable code in your system independent of the most volatile.

The Dependency Rule in Practice: A Complete Example

Consider an e-commerce system handling order placement:

[HTTP Layer]                     [Use Case Layer]              [Entity Layer]
Express Route Handler  -->  PlaceOrderInteractor  -->  Order.create()
                             |                          Order.calculateTotal()
                             v
                        OrderRepository (interface)
                             ^
                             |
[Persistence Layer]
PostgresOrderRepository (implements OrderRepository)

Dependencies:

  • Express Route Handler depends on PlaceOrderInteractor (inward) -- correct
  • PlaceOrderInteractor depends on Order (inward) -- correct
  • PlaceOrderInteractor depends on OrderRepository interface (same circle) -- correct
  • PostgresOrderRepository depends on OrderRepository interface (inward) -- correct
  • Express Route Handler does NOT appear in any inner circle -- correct
  • PostgreSQL does NOT appear in any inner circle -- correct

The Dependency Rule is satisfied. The business rules (Order, PlaceOrderInteractor) know nothing about Express or PostgreSQL. You could swap both without changing a single line of business logic.

1# The Dependency Rule and Concentric Circles
2 
3The Dependency Rule is the single most important concept in Clean Architecture. It states that source code dependencies can only point inward. Nothing in an inner circle can know anything at all about something in an outer circle. This includes names -- functions, classes, variables, data formats, or any other named software entity declared in an outer circle must not be mentioned by code in an inner circle.
4 
5This reference covers the concentric circles model, how data crosses boundaries, why direction matters, how frameworks violate the rule, and how to keep the inner circle pure.
6 
7## The Concentric Circles
8 
9Clean Architecture organizes code into concentric circles, each representing a different level of abstraction and policy. The innermost circles contain the highest-level, most general policies. The outermost circles contain the lowest-level, most concrete details.
10 
11### Circle 1: Entities (Innermost)
12 
13Entities encapsulate enterprise-wide business rules. These are the most general, most stable rules in the system. They are the least likely to change when something external changes -- a page navigation change, a security policy change, or a database migration should not affect entities.
14 
15**Characteristics of well-designed entities:**
16- They can be simple objects with methods, or they can be a set of data structures and functions
17- They encapsulate the most critical business rules
18- They have no dependency on anything in the outer circles
19- They would exist even if no software system existed (the rules are inherent to the business)
20- They are the most reusable elements across different applications in the enterprise
21 
22**Example:**
23 
24```python
25class LoanApplication:
26 def __init__(self, applicant_income: float, requested_amount: float, credit_score: int):
27 self.applicant_income = applicant_income
28 self.requested_amount = requested_amount
29 self.credit_score = credit_score
30 
31 def debt_to_income_ratio(self) -> float:
32 return self.requested_amount / (self.applicant_income * 12)
33 
34 def is_creditworthy(self) -> bool:
35 return self.credit_score >= 680 and self.debt_to_income_ratio() < 0.43
36```
37 
38This entity knows nothing about databases, HTTP, or frameworks. It encapsulates the business rule that determines creditworthiness.
39 
40### Circle 2: Use Cases
41 
42Use Cases contain application-specific business rules. They orchestrate the flow of data to and from entities and direct those entities to use their enterprise-wide business rules to achieve the goals of the use case.
43 
44**Characteristics:**
45- They define and implement input and output port interfaces
46- They manipulate entities to achieve application goals
47- Changes to use cases do not affect entities
48- Changes to external layers (database, UI) do not affect use cases
49 
50### Circle 3: Interface Adapters
51 
52This circle contains adapters that convert data between the format most convenient for the use cases and entities and the format most convenient for some external agency such as the database or the web.
53 
54**Contains:**
55- Controllers (translate inbound requests to use case input)
56- Presenters (translate use case output to external format)
57- Gateways (implement repository interfaces using specific technologies)
58 
59### Circle 4: Frameworks and Drivers (Outermost)
60 
61The outermost layer is composed of frameworks and tools -- the database, the web framework, the messaging system. This is where all the details go. The web is a detail. The database is a detail. We keep these things on the outside where they can do little harm.
62 
63**Contains:**
64- Web framework (Express, Spring, Django, Rails)
65- Database engine and ORM
66- External service clients
67- Device drivers and I/O
68 
69## The Direction of Dependencies
70 
71The arrow of dependency is not the same as the arrow of control flow. Control flow can go in any direction across a boundary. Source code dependencies, however, must always point inward.
72 
73### How Control Flow Opposes Dependency Direction
74 
75Consider this scenario: a controller needs to call a use case, and the use case needs to call a presenter. The control flows outward (from use case to presenter), but the dependency must point inward (presenter depends on use case, not the other way around).
76 
77The mechanism is Dependency Inversion:
78 
79```
80Controller --> [Use Case Input Port] <-- Use Case Interactor --> [Use Case Output Port] <-- Presenter
81```
82 
83The Use Case defines both the Input Port (which the Controller calls) and the Output Port (which the Presenter implements). The Use Case never knows about the Controller or the Presenter directly. It only knows about the interfaces it defines.
84 
85```python
86# Defined in the Use Case circle
87class PlaceOrderOutputPort(ABC):
88 @abstractmethod
89 def present_success(self, response: OrderResponse) -> None:
90 pass
91 
92 @abstractmethod
93 def present_failure(self, error: str) -> None:
94 pass
95 
96# Defined in the Use Case circle
97class PlaceOrderInteractor:
98 def __init__(self, order_repo: OrderRepository, presenter: PlaceOrderOutputPort):
99 self.order_repo = order_repo
100 self.presenter = presenter
101 
102 def execute(self, request: PlaceOrderRequest) -> None:
103 order = Order.create(request.items, request.customer_id)
104 self.order_repo.save(order)
105 self.presenter.present_success(OrderResponse(order.id, order.total))
106 
107# Defined in the Adapters circle -- implements the Use Case's interface
108class JsonOrderPresenter(PlaceOrderOutputPort):
109 def present_success(self, response: OrderResponse) -> None:
110 self.view_model = {"order_id": response.id, "total": str(response.total)}
111 
112 def present_failure(self, error: str) -> None:
113 self.view_model = {"error": error}
114```
115 
116The Interactor defines `PlaceOrderOutputPort`. The `JsonOrderPresenter` in the outer circle implements it. The dependency points inward even though control flows outward.
117 
118## Data Crossing Boundaries
119 
120When data crosses a boundary, it is always in the form that is most convenient for the inner circle. The outer circle must adapt its data into the form expected by the inner circle.
121 
122### Principle: Inner Circle Dictates Data Format
123 
124**Wrong -- outer circle format leaking inward:**
125 
126```python
127# Use Case receives a Django request object (framework dependency)
128class CreateUserInteractor:
129 def execute(self, request: HttpRequest): # VIOLATION: knows about Django
130 data = json.loads(request.body)
131 user = User(name=data['name'])
132```
133 
134**Right -- inner circle defines its own data structure:**
135 
136```python
137# Use Case defines its own request model
138@dataclass
139class CreateUserRequest:
140 name: str
141 email: str
142 
143class CreateUserInteractor:
144 def execute(self, request: CreateUserRequest): # Pure data structure
145 user = User(name=request.name, email=request.email)
146```
147 
148The Controller in the outer circle is responsible for translating the HTTP request into the `CreateUserRequest`.
149 
150### Crossing Data Patterns
151 
152| Pattern | When to Use | Example |
153|---------|-------------|---------|
154| **Request/Response DTOs** | Standard use case boundaries | `CreateOrderRequest` and `CreateOrderResponse` as plain data classes |
155| **Primitives** | Simple boundaries with few parameters | `get_user(user_id: str) -> UserResponse` |
156| **Domain events** | Communicating between bounded contexts | `OrderPlaced(order_id, timestamp)` emitted by inner circle |
157| **Data maps (dicts)** | Crossing boundaries where type safety is less critical | Acceptable in dynamic languages; prefer typed DTOs in static ones |
158 
159### What Must Not Cross Boundaries
160 
161- **ORM entities or database rows**: These are outer circle artifacts. Never pass an ActiveRecord model into a Use Case.
162- **Framework request/response objects**: `HttpRequest`, `HttpResponse`, `Request`, `Response` -- all belong in the outer circle.
163- **Third-party library types**: If your Use Case accepts an `AwsS3Object`, you've coupled business logic to AWS.
164 
165## How Frameworks Violate the Dependency Rule
166 
167Frameworks want to be the center of your universe. They ask you to subclass their base classes, decorate your code with their annotations, and structure your project according to their conventions. Every such demand is a dependency pointing outward-to-inward -- a violation.
168 
169### Common Framework Violations
170 
171| Framework Pattern | Violation | Fix |
172|-------------------|-----------|-----|
173| **ORM annotations on entities** | Entity depends on database framework | Separate domain entity from ORM model; map between them |
174| **Controller base classes** | Business logic inherits framework code | Use composition: controller holds a reference to the interactor |
175| **Framework-specific return types** | Use Case returns `ResponseEntity` or `JsonResponse` | Return plain DTOs; let the adapter format the response |
176| **Dependency injection via framework** | Inner circle annotated with `@Inject`, `@Autowired` | Use constructor injection with plain interfaces; wire in Main |
177| **Validation annotations** | Business validation tied to framework | Validate in the use case using plain code or a domain validator |
178 
179### Keeping Frameworks at Arm's Length
180 
181The key insight is to treat the framework as a plugin, not as your architecture:
182 
1831. **Don't derive from framework base classes** in your business logic. If the framework requires inheritance, create a thin adapter that inherits from the framework class and delegates to your clean inner code.
184 
1852. **Don't scatter framework annotations** throughout your domain. If you must use annotations for ORM mapping, do so on a separate persistence model that maps to and from your domain entity.
186 
1873. **Structure your project by business capability**, not by framework convention. Instead of `controllers/`, `models/`, `services/` (framework-driven), use `orders/`, `payments/`, `shipping/` (domain-driven), each with its own layers inside.
188 
189## Keeping the Inner Circle Pure
190 
191The inner circle is the most valuable part of the system because it contains the rules that make the business money. Protecting it from contamination requires vigilance.
192 
193### Purity Checklist
194 
195- **No imports from outer circles**: Grep your entity and use case code for imports of framework, database, or infrastructure packages. There should be none.
196- **No I/O**: Inner circle code never reads from a file, queries a database, or makes an HTTP call directly. It calls an interface, and the outer circle provides the implementation.
197- **No global state or singletons** that come from outer circles: If a use case accesses `Settings.DATABASE_URL`, it depends on infrastructure.
198- **No concurrency primitives** from the framework: Threads, async runtime, and event loops are outer circle concerns. Use cases should be synchronous-looking; the adapter handles async mechanics.
199- **Testable in isolation**: If you cannot instantiate a use case with mock implementations and run it without starting any server, database, or framework, the inner circle is not pure.
200 
201### Enforcement Strategies
202 
203| Strategy | How It Works | Tools |
204|----------|-------------|-------|
205| **Architecture tests** | Automated tests that verify import rules | ArchUnit (Java), Dependency Cruiser (JS/TS), import-linter (Python) |
206| **Module boundaries** | Language-level visibility (packages, modules) | Java modules, Go internal packages, Rust `pub(crate)` |
207| **Build system separation** | Inner and outer circles are separate build targets | Separate Gradle modules, npm packages, or Python packages |
208| **Code review rules** | Manual review for dependency direction violations | PR checklist: "Do any new imports in the domain cross outward?" |
209 
210### The Four-Step Inversion Process
211 
212When you discover an outward dependency in an inner circle:
213 
2141. **Identify the dependency**: What concrete outer-circle class is being referenced?
2152. **Define an interface in the inner circle** that describes what the inner circle needs (not what the outer circle provides).
2163. **Move the concrete implementation to the outer circle**, implementing the inner circle's interface.
2174. **Wire the dependency in Main** (the composition root), injecting the concrete implementation into the inner circle at startup.
218 
219This process always works. It may feel like ceremony, but it's the mechanism that keeps the most valuable code in your system independent of the most volatile.
220 
221## The Dependency Rule in Practice: A Complete Example
222 
223Consider an e-commerce system handling order placement:
224 
225```
226[HTTP Layer] [Use Case Layer] [Entity Layer]
227Express Route Handler --> PlaceOrderInteractor --> Order.create()
228 | Order.calculateTotal()
229 v
230 OrderRepository (interface)
231 ^
232 |
233[Persistence Layer]
234PostgresOrderRepository (implements OrderRepository)
235```
236 
237Dependencies:
238- Express Route Handler depends on PlaceOrderInteractor (inward) -- correct
239- PlaceOrderInteractor depends on Order (inward) -- correct
240- PlaceOrderInteractor depends on OrderRepository interface (same circle) -- correct
241- PostgresOrderRepository depends on OrderRepository interface (inward) -- correct
242- Express Route Handler does NOT appear in any inner circle -- correct
243- PostgreSQL does NOT appear in any inner circle -- correct
244 
245The Dependency Rule is satisfied. The business rules (Order, PlaceOrderInteractor) know nothing about Express or PostgreSQL. You could swap both without changing a single line of business logic.
246 

Discussion