Clean Architecture

Understand Clean Architecture's concentric circles — Entities, Use Cases, Interface Adapters, Frameworks & Drivers — and the Dependency Rule that governs how they may depend on each other, with a concrete PHP use-case example.

Clean Architecture, popularized by Robert C. Martin, generalizes the layered idea into a set of concentric circles with one non-negotiable law: the Dependency Rule. Source code dependencies can only point inward, toward higher-level policy; nothing in an inner circle can know anything about an outer circle. The goal is a system where business rules (the innermost circles) are completely independent of frameworks, databases, and the web — you could delete Laravel or swap MySQL for MongoDB without touching a single Entity or Use Case class.

The four concentric circles

Entities sit at the very center: enterprise-wide business rules and data structures that would be true regardless of the application — the most stable, least likely to change part of the system. Use Cases wrap the Entities: application-specific business rules, each one describing a single operation the system performs (RegisterUser, PlaceOrder, CancelSubscription), orchestrating Entities to accomplish it.

Interface Adapters convert data between the format Use Cases/Entities prefer and the format external agencies (the web, a database, the UI) prefer — this is where controllers, presenters, and gateways live. Frameworks & Drivers is the outermost circle: the actual framework, database engine, web server, and any other technical detail — this layer is treated as a plugin to everything inside it, not the other way around.

• Entities — enterprise business rules and core data structures; the most stable layer • Use Cases — application-specific business rules; one class per operation the system performs • Interface Adapters — controllers, presenters, gateways; translate between use cases and the outside world • Frameworks & Drivers — the actual framework, database, web server; details, kept as far out as possible

The Dependency Rule

The Dependency Rule states that source code dependencies must point only inward. Nothing in an inner circle can mention anything declared in an outer circle — not a class name, not a function, not any concept ('no rows,' no 'HTTP request') by name. When an inner circle needs to call something an outer circle provides (e.g., a Use Case needs to persist data), it defines an interface in the inner circle, and the outer circle implements it — the same dependency inversion used in layered architecture, applied consistently across all four rings.

This is why a Use Case class in Clean Architecture takes a UserRepository interface in its constructor rather than a concrete MysqlUserRepository or Eloquent\User — the Use Case circle must not know Frameworks & Drivers exists. Data that crosses a boundary should also be simple structures (arrays, DTOs), not framework-specific objects like an Eloquent Model or a PSR-7 Request, so the inner circles are never contaminated by outer-circle types.

Why the boundary is worth the extra ceremony

Clean Architecture asks for more upfront structure than a typical PHP script, and that cost is real — it is overkill for a five-page brochure site. The payoff shows up as the system grows: Use Cases and Entities can be unit tested with plain PHP and no database, the framework (and even the database engine) becomes replaceable without touching business rules, and multiple delivery mechanisms (a REST API, a GraphQL API, a CLI tool) can share the same Use Cases because none of them are baked into a specific outer-circle detail.

A Use Case class shaped by the Dependency Rule

RegisterUserUseCase never mentions PDO, Eloquent, or HTTP — only the interface and DTOs. The Input/Output objects are plain data, so the Use Case never leaks a framework type back out. User::register(...) keeps entity-level validation inside the Entity, not the Use Case.

// Use Case circle — depends only on an interface, defined in this circle or inward
final class RegisterUserUseCase
{
    public function __construct(private UserRepositoryInterface $users) {}

    public function execute(RegisterUserInput $input): RegisterUserOutput
    {
        $user = User::register($input->email, $input->password); // Entity rule
        $this->users->save($user);
        return new RegisterUserOutput($user->id(), $user->email());
    }
}

// Simple DTOs cross the boundary — never a framework Request/Model object
final class RegisterUserInput
{
    public function __construct(public readonly string $email, public readonly string $password) {}
}

final class RegisterUserOutput
{
    public function __construct(public readonly int $id, public readonly string $email) {}
}

Code Examples

The User entity enforces its own invariants, RegisterUserUseCase depends only on an interface it declares, RegisterUserController adapts a raw HTTP array into the use case's DTOs, and InMemoryUserRepository (a stand-in for a real database) is the outermost, swappable detail — dependencies point inward at every step. Expected result: Array ( [id] => 1 [email] => [email protected]) final class RegisterUserUseCase { public function __construct(private UserRepositoryInterface $users) {} } — the Use Case never names a concrete repository or framework class.

<?php

// --- Entities circle: enterprise-wide rule, no framework knowledge ---
final class User
{
    private function __construct(
        private ?int $id,
        private string $email,
        private string $passwordHash
    ) {}

    public static function register(string $email, string $plainPassword): self
    {
        if (!str_contains($email, '@')) {
            throw new \InvalidArgumentException('Invalid email address.');
        }
        if (strlen($plainPassword) < 8) {
            throw new \InvalidArgumentException('Password must be at least 8 characters.');
        }
        return new self(null, $email, password_hash($plainPassword, PASSWORD_DEFAULT));
    }

    public function id(): ?int { return $this->id; }
    public function email(): string { return $this->email; }
    public function withId(int $id): self
    {
        $clone = clone $this;
        $clone->id = $id;
        return $clone;
    }
}

// --- Use Cases circle: defines the interface it needs, orchestrates Entities ---
interface UserRepositoryInterface
{
    public function nextId(): int;
    public function save(User $user): User;
}

final class RegisterUserInput
{
    public function __constr
…

Clean Architecture concentric circles

Entities know nothing about Use Cases; Use Cases know nothing about Interface Adapters; nothing inside knows Frameworks & Drivers exists.

┌───────────────────────────────────────┐
        │        Frameworks & Drivers             │
        │   ┌───────────────────────────────┐    │
        │   │      Interface Adapters         │    │
        │   │   ┌───────────────────────┐    │    │
        │   │   │      Use Cases         │    │    │
        │   │   │   ┌───────────────┐   │    │    │
        │   │   │   │   Entities     │   │    │    │
        │   │   │   └───────────────┘   │    │    │
        │   │   └───────────────────────┘    │    │
        │   └───────────────────────────────┘    │
        └───────────────────────────────────────┘
              Dependency Rule: all arrows point INWARD only →

Important Rules

The Dependency Rule: source code dependencies may only point inward, from outer circles toward Entities.

Inner circles must not name, import, or reference anything declared in an outer circle — not even by concept.

When an inner circle needs an outer-circle capability, it defines the interface; the outer circle implements it.

Data crossing a circle boundary should be simple structures (DTOs/arrays), never framework types like an Eloquent Model or PSR-7 Request.

Clean Architecture in real PHP codebases

Teams building long-lived PHP systems (banking, healthcare, large e-commerce platforms) adopt Clean Architecture specifically so the business rules outlive any one framework version. When Laravel or Symfony releases a major version with breaking changes, or the team migrates from MySQL to PostgreSQL, only the outer Frameworks & Drivers and Interface Adapters circles need to change — the Entities and Use Cases, which encode the actual business, are untouched and their tests keep passing throughout.

final class RegisterUserUseCase { public function execute(\Illuminate\Http\Request $request) { $user = \App\Models\User::create($request->all()); } } final class RegisterUserUseCase { public function __construct(private UserRepositoryInterface $users) {} public function execute(RegisterUserInput $input): RegisterUserOutput { /*... */ } }

The wrong version imports a Laravel HTTP Request and an Eloquent Model directly into the Use Case circle, violating the Dependency Rule by making inner-circle code depend on outer-circle framework types.

According to the Dependency Rule, which direction may source code dependencies point in Clean Architecture? A. Outward only, from Entities toward Frameworks & Drivers B. Inward only, from outer circles toward Entities C. Either direction, as long as interfaces are used D. Sideways only, between Interface Adapters and Use Cases

B. Inward only, from outer circles toward Entities — The Dependency Rule requires all source code dependencies to point inward, toward higher-level policy (Entities), never the reverse.

Why should a Use Case class avoid taking a framework's Request object or an ORM Model as a parameter? A. Because PHP does not support type hints for framework classes B. Because it would make the code run slower C. Because it would make the inner Use Case circle depend on an outer Frameworks & Drivers circle type, violating the Dependency Rule D. Because framework objects cannot be serialized

C. Because it would make the inner Use Case circle depend on an outer Frameworks & Drivers circle type, violating the Dependency Rule — Accepting a framework-specific type inside a Use Case pulls an outer-circle dependency into an inner circle, which the Dependency Rule forbids — plain DTOs should cross that boundary instead.

You have a CancelSubscriptionUseCase that currently type-hints Stripe\Subscription (a Frameworks & Drivers class) directly in its execute() method. Redesign it so it respects the Dependency Rule.

Define a SubscriptionGatewayInterface with a cancel(string $subscriptionId): void method in the Use Case (or Entities) circle. CancelSubscriptionUseCase depends on that interface. A StripeSubscriptionGateway implements SubscriptionGatewayInterface in the Frameworks & Drivers circle and wraps the actual Stripe\Subscription calls.

What interface could the Use Case circle define instead of depending on Stripe's SDK type directly?