When codebases grow, they tend toward disorder. Features become tangled together. Changing one thing breaks another. Testing becomes difficult or impossible. Clean Architecture offers a way out;a set of principles for organizing code that remains maintainable as applications scale.
What Is Clean Architecture?
Clean Architecture, popularized by Robert C. Martin, organizes code into concentric layers. Each layer has specific responsibilities and dependencies that only point inward, toward the core business logic.
The key insight is separating what your application does (business rules) from how it does it (frameworks, databases, APIs). This separation makes your code easier to test, modify, and understand.
The Dependency Rule
The fundamental rule of Clean Architecture: source code dependencies must only point inward. Inner layers cannot know anything about outer layers.
Your business logic shouldn't know whether data comes from MySQL or MongoDB. It shouldn't know whether requests come via HTTP or CLI. These are details that belong in outer layers.
This rule creates flexibility. You can swap databases, change frameworks, or add new interfaces without touching your core business logic.
Understanding the Layers
Entities (Domain Layer)
Entities encapsulate enterprise-wide business rules. They're the most stable part of your application;the logic that exists independent of any particular system.
In a project management application, entities might include:
class Project
{
private ProjectId $id;
private string $name;
private ProjectStatus $status;
private Money $budget;
private Collection $tasks;
public function markComplete(): void
{
if ($this->hasIncompleteTasks()) {
throw new CannotCompleteProjectException('All tasks must be complete');
}
$this->status = ProjectStatus::Completed;
}
public function isOverBudget(): bool
{
return $this->totalSpent()->greaterThan($this->budget);
}
}
Entities contain business rules that would exist even if there were no software system. "A project cannot be marked complete while it has incomplete tasks" is a business rule, not a framework concern.
Use Cases (Application Layer)
Use cases orchestrate the flow of data to and from entities. They implement application-specific business rules.
class CompleteProjectUseCase
{
public function __construct(
private ProjectRepository $projects,
private NotificationService $notifications,
) {}
public function execute(CompleteProjectRequest $request): CompleteProjectResponse
{
$project = $this->projects->findById($request->projectId);
if (!$project) {
throw new ProjectNotFoundException();
}
$project->markComplete();
$this->projects->save($project);
$this->notifications->notifyProjectComplete($project);
return new CompleteProjectResponse($project);
}
}
Use cases know about entities but don't know about controllers, databases, or frameworks. They work with interfaces (like ProjectRepository) rather than concrete implementations.
Interface Adapters
This layer converts data between the format used by use cases and the format needed by external systems.
Controllers adapt HTTP requests to use case inputs:
class ProjectController
{
public function complete(Request $request, CompleteProjectUseCase $useCase)
{
$response = $useCase->execute(
new CompleteProjectRequest($request->route('id'))
);
return response()->json([
'id' => $response->project->id()->toString(),
'status' => $response->project->status()->value,
]);
}
}
Repositories implement the interfaces defined in inner layers:
class EloquentProjectRepository implements ProjectRepository
{
public function findById(ProjectId $id): ?Project
{
$model = ProjectModel::find($id->toString());
return $model ? $this->toDomain($model) : null;
}
private function toDomain(ProjectModel $model): Project
{
return new Project(
ProjectId::fromString($model->id),
$model->name,
ProjectStatus::from($model->status),
Money::fromCents($model->budget_cents),
);
}
}
Frameworks and Drivers
The outermost layer contains frameworks, tools, and delivery mechanisms. Laravel itself lives here. So do your database drivers, web servers, and external APIs.
This layer contains glue code that connects everything together;service providers, route definitions, middleware configuration.
Implementing in Laravel
Laravel's conventions don't naturally align with Clean Architecture, but they can coexist.
Directory Structure
Organize code by layer, not by Laravel convention:
app/
├── Domain/
│ ├── Project/
│ │ ├── Project.php
│ │ ├── ProjectId.php
│ │ ├── ProjectStatus.php
│ │ └── ProjectRepository.php (interface)
│ └── Task/
│ └── ...
├── Application/
│ └── Project/
│ ├── CompleteProjectUseCase.php
│ ├── CompleteProjectRequest.php
│ └── CompleteProjectResponse.php
├── Infrastructure/
│ ├── Persistence/
│ │ └── EloquentProjectRepository.php
│ └── Notifications/
│ └── MailNotificationService.php
└── Http/
└── Controllers/
└── ProjectController.php
Service Container Binding
Use Laravel's service container to inject implementations:
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(
ProjectRepository::class,
EloquentProjectRepository::class
);
$this->app->bind(
NotificationService::class,
MailNotificationService::class
);
}
}
DTOs and Value Objects
Use Data Transfer Objects to cross boundaries:
readonly class CreateProjectRequest
{
public function __construct(
public string $name,
public string $description,
public int $budgetCents,
public string $clientId,
) {}
public static function fromHttpRequest(Request $request): self
{
return new self(
$request->input('name'),
$request->input('description'),
(int) ($request->input('budget') * 100),
$request->input('client_id'),
);
}
}
Value Objects encapsulate validation and behavior:
readonly class Money
{
private function __construct(private int $cents) {}
public static function fromCents(int $cents): self
{
if ($cents < 0) {
throw new InvalidArgumentException('Money cannot be negative');
}
return new self($cents);
}
public function greaterThan(Money $other): bool
{
return $this->cents > $other->cents;
}
}
When Clean Architecture Is Overkill
Clean Architecture adds indirection. That indirection has costs: more files, more abstractions, more complexity to navigate.
For simple CRUD applications, the overhead often exceeds the benefit. If you're building a straightforward admin panel with minimal business logic, Laravel's default structure works fine.
Consider Clean Architecture when:
- Business logic is complex and likely to change
- You need to support multiple interfaces (web, API, CLI)
- The application will be maintained for years
- Multiple teams work on the codebase
- High test coverage is required
Skip it when:
- Building a prototype or MVP
- Business logic is minimal (data in, data out)
- The team is small and development speed is critical
- The application has a limited lifespan
Practical Example
Here's a complete flow for creating a project:
// Domain: app/Domain/Project/Project.php
class Project
{
public static function create(string $name, Money $budget, ClientId $clientId): self
{
return new self(
ProjectId::generate(),
$name,
ProjectStatus::Draft,
$budget,
$clientId,
);
}
}
// Application: app/Application/Project/CreateProjectUseCase.php
class CreateProjectUseCase
{
public function execute(CreateProjectRequest $request): CreateProjectResponse
{
$project = Project::create(
$request->name,
Money::fromCents($request->budgetCents),
ClientId::fromString($request->clientId),
);
$this->projects->save($project);
return new CreateProjectResponse($project);
}
}
// Infrastructure: app/Http/Controllers/ProjectController.php
class ProjectController
{
public function store(Request $request, CreateProjectUseCase $useCase)
{
$response = $useCase->execute(
CreateProjectRequest::fromHttpRequest($request)
);
return response()->json(['id' => $response->project->id()->toString()], 201);
}
}
Conclusion
Clean Architecture isn't about following rules for their own sake. It's about creating boundaries that protect your most valuable code;your business logic;from the volatility of frameworks and infrastructure.
Start small. Extract business rules into domain objects. Introduce interfaces where you need flexibility. Add layers as complexity demands. The goal isn't architectural purity; it's maintainable code that you can confidently change and test.
Clean Architecture scales with your needs. A small application might only have domain objects and use cases. A complex system might have multiple bounded contexts, each with full layering. Let your application's complexity drive how much architecture you need.