Loading content...
If Clean Architecture can be reduced to a single principle, it is the Dependency Rule. This rule is both elegant in its simplicity and profound in its implications:
Source code dependencies must only point inward.
Nothing in an inner circle can know anything at all about something in an outer circle. The name of something declared in an outer circle must not be mentioned by code in an inner circle. That includes functions, classes, variables, or any other named software entity.
This single rule—properly understood and rigorously applied—produces systems that are testable without infrastructure, independent of frameworks, and adaptable to technological change. It is the architectural equivalent of a physical law: simple to state, far-reaching in consequences.
By the end of this page, you will deeply understand the Dependency Rule—not just its statement, but its rationale, its enforcement mechanisms, and the patterns that emerge from its consistent application. You will see how this single principle creates the separation that makes Clean Architecture 'clean.'
Let's break down the rule with precision:
"Source code dependencies" refers to compile-time or import-time dependencies. In Java, it's what you import. In TypeScript, what you import or require. In Python, what you import. These are the dependencies that the compiler or interpreter must resolve.
"Must only point inward" means that if you're in an outer circle, you may reference things in inner circles. But if you're in an inner circle, you may NOT reference things in outer circles.
Visualizing the Direction:
Frameworks & Drivers →→→ Interface Adapters →→→ Use Cases →→→ Entities
(outer) (inner)
Dependencies flow from left to right—from volatile outer components toward stable inner components. The inner components (business rules) are the most stable; they change only when business policies change. The outer components (frameworks, UI) are the most volatile; they change with technology trends.
What the Rule Forbids:
Every import statement creates a coupling. When you write import { HttpRequest } from 'express' in your use case, you've created a dependency on Express. If Express changes or you want to switch to Fastify, your use case must change—even though HTTP handling has nothing to do with your business logic.
The Dependency Rule isn't arbitrary. It reflects a fundamental truth about software stability and change:
Stability Gradient
Not all parts of a system change at the same rate:
By making dependencies point from volatile to stable, we ensure that changes in volatile components don't ripple into stable components. A framework upgrade shouldn't force changes to business rules.
The Direction of Impact
When dependencies point inward:
When dependencies point outward (violation):
The Economic Argument
Changes to inner circles are expensive because they ripple outward. Changes to outer circles are cheap because nothing depends on them. The Dependency Rule ensures that:
This is economically optimal. You invest stability where stability matters most.
The Dependency Rule creates an apparent paradox: How can inner layers accomplish anything if they can't reference outer layers? A use case needs to save data to a database, but it can't import the database module. A use case needs to send output to a UI, but it can't import the presenter.
The solution is the Dependency Inversion Principle applied at architectural scale.
The Paradox:
// Use Case Layer - Inner Circle
class TransferFunds {
execute(from: string, to: string, amount: number): void {
// I need to save to database...
// But I can't import DatabaseRepository!
// That would violate the Dependency Rule!
}
}
The Resolution:
The use case doesn't reference the concrete database implementation. Instead, it references an abstraction (interface) that lives in its own layer. The concrete implementation, which lives in an outer layer, depends on that abstraction.
// Use Case Layer - Defines what it needs
interface AccountRepository {
findById(id: string): Account | null;
save(account: Account): void;
}
class TransferFunds {
constructor(private accountRepo: AccountRepository) {}
execute(from: string, to: string, amount: number): void {
const fromAccount = this.accountRepo.findById(from);
const toAccount = this.accountRepo.findById(to);
// ... business logic ...
this.accountRepo.save(fromAccount);
this.accountRepo.save(toAccount);
}
}
// Adapter Layer (outer) - Implements the abstraction
import { AccountRepository, Account } from '../use-cases/ports';
import { DatabaseConnection } from 'some-database-library';
class PostgresAccountRepository implements AccountRepository {
constructor(private db: DatabaseConnection) {}
findById(id: string): Account | null {
const row = this.db.query('SELECT * FROM accounts WHERE id = $1', [id]);
return row ? this.mapToAccount(row) : null;
}
save(account: Account): void {
this.db.execute(
'UPDATE accounts SET balance = $1 WHERE id = $2',
[account.balance, account.id]
);
}
}
Notice the dependency direction: the adapter imports from the use case layer, not vice versa. The use case knows nothing about PostgreSQL.
A crucial insight: the interface (AccountRepository) is defined in the use case layer—the layer that uses it—not the infrastructure layer that implements it. This is Dependency Inversion: high-level modules define abstractions that low-level modules implement.
The Dependency Rule applies to code dependencies, but data must still flow across boundaries. How do we handle this without creating coupling?
The Rule for Data:
When passing data across a boundary, always pass it in a form that is convenient for the inner circle. The outer circle adapts to the inner circle's expectations, never the reverse.
Example: HTTP Request to Use Case
The HTTP framework (outer) receives a request with its own structure. The use case (inner) defines what input it expects:
// Use Case Layer - Defines its own input format
interface TransferFundsInput {
fromAccountId: string;
toAccountId: string;
amount: number;
currency: string;
}
interface TransferFundsOutput {
success: boolean;
newFromBalance: number;
newToBalance: number;
transactionId: string;
}
class TransferFunds {
execute(input: TransferFundsInput): TransferFundsOutput {
// ... business logic ...
}
}
// Adapter Layer - Converts HTTP to use case format
import { TransferFundsInput } from '../use-cases/transfer-funds';
import express from 'express';
class TransferFundsController {
constructor(private useCase: TransferFunds) {}
async handle(req: express.Request, res: express.Response) {
// Convert HTTP request to use case input
const input: TransferFundsInput = {
fromAccountId: req.body.from,
toAccountId: req.body.to,
amount: parseFloat(req.body.amount),
currency: req.body.currency || 'USD',
};
const output = this.useCase.execute(input);
// Convert use case output to HTTP response
res.json({
ok: output.success,
transaction_id: output.transactionId,
balances: {
from: output.newFromBalance,
to: output.newToBalance,
},
});
}
}
Notice:
The Dependency Rule is only useful if it's enforced. Without enforcement, developers under pressure will take shortcuts, and the architecture will degrade. Here are mechanisms for enforcement:
1. Module Boundaries (Compile-time)
Many languages support modules or packages with visibility controls:
2. Architectural Fitness Functions (CI/CD)
Automated tests that verify architectural constraints:
// Using ArchUnit (Java) or similar
describe('Architecture Rules', () => {
it('use cases should not import from adapters', () => {
const violations = analyzeImports('src/use-cases/**/*')
.filter(imp => imp.startsWith('src/adapters'));
expect(violations).toHaveLength(0);
});
it('entities should not import from use cases', () => {
const violations = analyzeImports('src/entities/**/*')
.filter(imp => imp.startsWith('src/use-cases'));
expect(violations).toHaveLength(0);
});
});
3. Linters and Static Analysis
Tools like ESLint (with eslint-plugin-import) or custom rules can flag violations at development time:
// .eslintrc.js
module.exports = {
rules: {
'import/no-restricted-paths': ['error', {
zones: [
{
target: './src/entities',
from: './src/use-cases',
message: 'Entities cannot import from use cases'
},
{
target: './src/use-cases',
from: './src/adapters',
message: 'Use cases cannot import from adapters'
}
]
}]
}
};
4. Code Review
Ultimately, code review by team members who understand the architecture catches violations that automated tools miss.
The best enforcement combines multiple mechanisms: compile-time module boundaries (catches most violations), CI fitness functions (catches anything that slips through), linters (fast feedback for developers), and code review (catches subtle violations and educates the team).
Let's see the Dependency Rule play out in a realistic scenario. Consider an e-commerce system handling order placement:
Layer Structure:
src/
├── entities/ # Enterprise Business Rules
│ ├── order.ts
│ ├── order-item.ts
│ ├── customer.ts
│ └── product.ts
├── use-cases/ # Application Business Rules
│ ├── place-order.ts
│ └── ports/
│ ├── order-repository.ts # Interface
│ ├── inventory-service.ts # Interface
│ └── payment-gateway.ts # Interface
├── adapters/ # Interface Adapters
│ ├── controllers/
│ │ └── order-controller.ts
│ ├── repositories/
│ │ └── postgres-order-repository.ts
│ ├── gateways/
│ │ └── stripe-payment-gateway.ts
│ └── services/
│ └── http-inventory-service.ts
└── frameworks/ # Frameworks & Drivers
├── express-app.ts
├── database.ts
└── config.ts
12345678910111213141516171819202122232425262728293031323334353637383940414243
// Entity Layer - No external dependencies// This file knows nothing about databases, HTTP, or frameworks import { OrderItem } from './order-item';import { Customer } from './customer'; export class Order { private items: OrderItem[] = []; private status: OrderStatus = 'pending'; constructor( public readonly id: string, public readonly customer: Customer, public readonly createdAt: Date = new Date() ) {} addItem(item: OrderItem): void { if (this.status !== 'pending') { throw new Error('Cannot add items to a non-pending order'); } this.items.push(item); } getTotalAmount(): number { return this.items.reduce( (sum, item) => sum + (item.price * item.quantity), 0 ); } canBeFulfilled(): boolean { return this.items.every(item => item.isAvailable); } confirm(): void { if (!this.canBeFulfilled()) { throw new Error('Cannot confirm order with unavailable items'); } this.status = 'confirmed'; }} type OrderStatus = 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
// Use Case Layer - Imports only from entities and its own ports// Never imports from adapters or frameworks import { Order, OrderItem } from '../entities';import { OrderRepository } from './ports/order-repository';import { InventoryService } from './ports/inventory-service';import { PaymentGateway } from './ports/payment-gateway'; export interface PlaceOrderInput { customerId: string; items: Array<{ productId: string; quantity: number }>; paymentMethodId: string;} export interface PlaceOrderOutput { orderId: string; totalAmount: number; status: string;} export class PlaceOrder { constructor( private orderRepo: OrderRepository, private inventory: InventoryService, private payments: PaymentGateway ) {} async execute(input: PlaceOrderInput): Promise<PlaceOrderOutput> { // Check inventory const availability = await this.inventory.checkAvailability( input.items.map(i => ({ productId: i.productId, quantity: i.quantity })) ); if (!availability.allAvailable) { throw new Error('Some items are not available'); } // Create order entity const order = new Order( generateOrderId(), await this.orderRepo.getCustomer(input.customerId) ); for (const item of input.items) { order.addItem(new OrderItem( item.productId, item.quantity, availability.prices[item.productId] )); } // Process payment const paymentResult = await this.payments.charge( input.paymentMethodId, order.getTotalAmount() ); if (!paymentResult.success) { throw new Error('Payment failed'); } // Confirm and save order.confirm(); await this.orderRepo.save(order); return { orderId: order.id, totalAmount: order.getTotalAmount(), status: 'confirmed' }; }}Notice how the use case imports only from entities and its own ports directory. It has no idea whether the OrderRepository uses PostgreSQL, MongoDB, or in-memory storage. It doesn't know if PaymentGateway is Stripe, PayPal, or a test fake.
Even experienced teams sometimes violate the Dependency Rule. Here are common violations and how to fix them:
Violation 1: Framework Types in Use Cases
12345678910
// Use Case - BADimport { Request, Response } from 'express'; class GetUser { execute(req: Request, res: Response) { const userId = req.params.id; // ... business logic ... res.json(user); }}123456789101112131415161718192021
// Use Case - GOODinterface GetUserInput { userId: string;} class GetUser { execute(input: GetUserInput): User { // ... business logic ... return user; }} // Controller handles Expressclass UserController { handle(req: Request, res: Response) { const result = this.getUser.execute({ userId: req.params.id }); res.json(result); }}Violation 2: ORM Annotations on Entities
123456789101112131415
// Entity - BADimport { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity()class User { @PrimaryGeneratedColumn() id: string; @Column() name: string; @Column() email: string;}12345678910111213141516171819
// Entity - GOOD (pure domain)class User { constructor( public readonly id: string, public readonly name: string, public readonly email: string ) {}} // Adapter Layer - DB Model@Entity('users')class UserDbModel { @PrimaryGeneratedColumn() id: string; @Column() name: string; // Repository maps between themViolation 3: Direct Database Calls in Use Cases
1234567891011121314
// Use Case - BADimport { Pool } from 'pg'; class GetOrderHistory { constructor(private db: Pool) {} async execute(userId: string) { const result = await this.db.query( 'SELECT * FROM orders WHERE user_id = $1', [userId] ); return result.rows; }}12345678910111213141516171819202122
// Use Case - GOODinterface OrderRepository { findByUserId(userId: string): Order[];} class GetOrderHistory { constructor(private repo: OrderRepository) {} async execute(userId: string) { return this.repo.findByUserId(userId); }} // Adapter implements the interfaceclass PgOrderRepository implements OrderRepository { constructor(private db: Pool) {} async findByUserId(userId: string) { const result = await this.db.query(/*...*/); return result.rows.map(toOrder); }}When the Dependency Rule is consistently followed, several powerful benefits emerge:
Testability Without Infrastructure
Business rules can be tested with simple unit tests. No database connections, no HTTP servers, no message queues. Tests run in milliseconds:
describe('TransferFunds', () => {
it('should transfer amount between accounts', () => {
const fakeRepo = new InMemoryAccountRepository();
fakeRepo.save(new Account('A', 100));
fakeRepo.save(new Account('B', 50));
const useCase = new TransferFunds(fakeRepo);
useCase.execute({ from: 'A', to: 'B', amount: 30 });
expect(fakeRepo.findById('A').balance).toBe(70);
expect(fakeRepo.findById('B').balance).toBe(80);
});
});
Technology Flexibility
Changing databases requires only writing a new adapter—no changes to business logic:
// Today: PostgreSQL
const orderRepo = new PostgresOrderRepository(pgPool);
// Tomorrow: MongoDB
const orderRepo = new MongoOrderRepository(mongoClient);
// The use case doesn't change:
const placeOrder = new PlaceOrder(orderRepo, inventory, payments);
Parallel Development
Teams can work independently. The UI team builds controllers and presenters. The database team builds repositories. The core team builds use cases and entities. Interfaces define the contracts.
Incremental Adoption
You don't need to adopt Clean Architecture everywhere at once. Start with one use case. Define its ports. Implement adapters. Gradually expand.
The Dependency Rule is the architectural law that makes Clean Architecture work. Let's consolidate:
What's Next:
Now that we understand the Dependency Rule, we'll examine the specific components that live in each layer: Entities, Use Cases, Adapters, and Frameworks. Each has distinct responsibilities and design principles.
You now deeply understand the Dependency Rule—the single principle that governs all of Clean Architecture. It's not enough to know it; you must enforce it. In the next page, we'll explore what belongs in each architectural layer.