Skip to main content
Clean Architecture 80 XP · 8 min

The Dependency Rule in Clean Architecture

Source code dependencies can only point inward — nothing in an inner circle can know anything about an outer circle, keeping the business core stable and reusable.

Showing
Ad (728×90)

Why this matters

Clean Architecture is organized as four concentric circles: Entities (innermost) → Use CasesInterface AdaptersFrameworks & Drivers (outermost). The Dependency Rule is the single constraint that makes this work: source code dependencies can only point inward.

Nothing in an inner circle can know anything about something in an outer circle. The name of a function, a class, a variable, or any software entity declared in an outer circle must not be mentioned in the code of an inner circle. This includes data formats used in an outer circle — data that crosses a boundary must be converted to simple DTO form that the inner circle knows about.

This rule is what makes the core reusable. An Entity that imports SQLAlchemy is not reusable without SQLAlchemy. A Use Case that imports Flask is not testable without Flask. The inner circles are stable precisely because nothing in them reaches outward. Every import in your domain layer is a potential violation.

The problem

An Order entity (innermost circle) that imports SQLAlchemy, Flask, and Stripe simultaneously — the innermost layer depends on all three outermost layers.

Bad

from sqlalchemy import Column, Float, Integer
from sqlalchemy.ext.declarative import declarative_base
from flask import request
import stripe

Base = declarative_base()

class Order(Base):                  # depends on SQLAlchemy (outermost)
    __tablename__ = "orders"
    id    = Column(Integer, primary_key=True)
    total = Column(Float)

    def charge(self):
        token = request.json["token"]  # depends on Flask (outermost)
        stripe.Charge.create(          # depends on Stripe (outermost)
            amount=int(self.total * 100),
            currency="usd",
            source=token,
        )

# The innermost circle depends on three outermost circles.
# Cannot test charge() without Flask running and Stripe credentials.
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; // Frameworks
import { Request } from "express";                                // Frameworks
import Stripe from "stripe";                                      // Frameworks

@Entity()
export class Order {
  @PrimaryGeneratedColumn() id!: number;
  @Column("float")          total!: number;

  async charge(req: Request): Promise {
    const stripe = new Stripe(process.env.STRIPE_KEY!, { apiVersion: "2023-10-16" });
    const token  = (req.body as any).token;
    await stripe.charges.create({ amount: Math.round(this.total * 100), currency: "usd", source: token });
  }
}
// Entity (innermost) imports Framework layer (outermost) — Dependency Rule violated.

The solution

A pure Order entity with only domain state and rules. Outer layers (StripeGateway, PlaceOrderUseCase) know about Order — Order knows nothing of them.

Good

from dataclasses import dataclass
from decimal import Decimal

@dataclass
class Order:
    order_id: str
    items: list
    status: str = "pending"

    def calculate_total(self) -> Decimal:
        """Critical Business Rule — exists regardless of payment provider."""
        return sum(item.subtotal() for item in self.items)

    def mark_paid(self) -> None:
        """Domain rule — an order can only be paid once."""
        if self.status != "pending":
            raise ValueError(f"Cannot pay order with status: {self.status}")
        self.status = "paid"

# Outer layers depend on Order; Order depends on nothing outer.
# PaymentService (Use Cases) calls order.mark_paid() after Stripe succeeds.
# StripeGateway (Interface Adapters) knows about Stripe; Order does not.
# Test: order = Order("1", items); order.mark_paid()  — no mocks needed.
export class Order {
  private _status: "pending" | "paid" | "cancelled" = "pending";

  constructor(readonly orderId: string, readonly items: readonly OrderItem[]) {}

  get status() { return this._status; }

  calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.subtotal(), 0);
  }

  markPaid(): void {
    // Domain rule — independent of any payment provider
    if (this._status !== "pending") {
      throw new Error(\`Cannot pay an order with status: \${this._status}\`);
    }
    this._status = "paid";
  }
}

// StripePaymentGateway (Interface Adapters) knows about Stripe; Order does not.
// PlaceOrderUseCase (Use Cases) calls order.markPaid() after payment succeeds.
// Test: const order = new Order("1", items); order.markPaid(); — zero mocks.

Key takeaway

The Dependency Rule is the single most important constraint in Clean Architecture. Source code dependencies must only point inward — inner circles know nothing of outer circles. Every import in your domain layer is a potential violation. The Entities are stable because they depend on nothing that can change beneath them.

Done with this lesson?

Mark it complete to earn XP and track your progress.