TL;DR
π‘ Clean Architecture isn't about folders β it's a mindset
β οΈ 73% of projects die from technical debt
β
4 layers will save your project from a $100k refactor
π Ready-to-use folder structure at the end
βββββββββββββββββββββββββββββ
π± The Problem: Why Everything Gets Worse
I remember my first "real" project. After 6 months, I was scared to touch my own code. Adding a field to a form? 2 hours. Changing discount logic? A prayer and 3 hours.
Sound familiar? Here are typical "spaghetti code" symptoms:
β οΈ Common Mistakes:
- 500-line controllers (hello, God Object!)
- Business logic inside SQL queries
- "Quick fixes" directly in templates
- Impossible to test without a database
π¬ "Good architecture costs more upfront. Bad architecture costs 10x more later."
β Every senior dev's experience
βββββββββββββββββββββββββββββ
π― The Solution: Clean Architecture Explained
What is it anyway?
Clean Architecture by Robert Martin ("Uncle Bob") isn't about Symfony or Laravel. It's about the Dependency Rule:
πΊ Outer layers depend on inner layers. Never the other way around.
βββββββββββββββββββββββββββββββββββ
β Presentation (UI, API) β β knows about Application
βββββββββββββββββββββββββββββββββββ€
β Application (Use Cases) β β knows about Domain
βββββββββββββββββββββββββββββββββββ€
β Domain (Business Logic) β β knows NOTHING!
βββββββββββββββββββββββββββββββββββ€
β Infrastructure (DB, HTTP) β β implements Domain interfaces
βββββββββββββββββββββββββββββββββββ
ποΈ The 4 Layers Simplified
1οΈβ£ Domain Layer β Business Core
This is your application's heart. Entities that your business can't exist without.
// Domain/Entity/Order.php
final class Order
{
private OrderId $id;
private Money $total;
private OrderStatus $status;
public static function create(Email $customerEmail, Money $total): self
{
$order = new self(OrderId::generate(), $total);
$order->recordEvent(new OrderCreated($order->id));
return $order;
}
// β NO mentions of Doctrine, HTTP, JSON!
}
π‘ Key Idea: Domain doesn't know about databases, APIs, or frameworks.
2οΈβ£ Application Layer β The Orchestrator
Here live Use Cases (user scenarios). Receive request β call domain β return result.
// Application/UseCase/CreateOrder/CreateOrderHandler.php
final class CreateOrderHandler
{
public function __construct(
private OrderRepositoryInterface $repo,
private EventBus $eventBus
) {}
public function __invoke(CreateOrderCommand $cmd): CreateOrderResponse
{
// 1. Validate input
$email = Email::fromString($cmd->customerEmail);
$total = Money::from($cmd->total, 'USD');
// 2. Execute business logic
$order = Order::create($email, $total);
// 3. Persist
$this->repo->save($order);
$this->eventBus->dispatch(new OrderCreated($order->getId()));
return new CreateOrderResponse($order->getId());
}
}
β Notice: Handler coordinates but doesn't contain business rules.
3οΈβ£ Infrastructure Layer β The Dirty Work
Doctrine, Redis, HTTP clients, email services. All "technical stuff".
// Infrastructure/Repository/DoctrineOrderRepository.php
final class DoctrineOrderRepository implements OrderRepositoryInterface
{
public function save(Order $order): void
{
// Map Domain Entity β Doctrine Entity
$entity = $this->mapper->toDoctrineEntity($order);
$this->entityManager->persist($entity);
$this->entityManager->flush();
}
}
4οΈβ£ Presentation Layer β Entry Point
Controllers, CLI commands, GraphQL resolvers.
// Infrastructure/Presentation/Controller/CreateOrderController.php
final class CreateOrderController
{
public function __invoke(
Request $request,
CreateOrderHandler $handler
): JsonResponse {
$cmd = new CreateOrderCommand(
customerEmail: $request->get('email'),
items: $request->get('items')
);
$response = $handler($cmd);
return $this->json(['orderId' => $response->orderId], 201);
}
}
βββββββββββββββββββββββββββββ
π₯ Real Example: Before vs After
β Before (Classic MVC Nightmare)
// Controller doing EVERYTHING
class OrderController
{
public function create(Request $request)
{
// Validation in controller π±
if (!filter_var($request->get('email'), FILTER_VALIDATE_EMAIL)) {
return $this->json(['error' => 'Invalid email'], 400);
}
// Business logic in controller π±
$total = 0;
foreach ($request->get('items') as $item) {
$total += $item['price'] * $item['qty'];
}
// Direct DB access π±
$order = new Order();
$order->setEmail($request->get('email'));
$order->setTotal($total);
$this->em->persist($order);
$this->em->flush();
// Email sending in controller π±
$this->mailer->send('Order created!');
return $this->json($order);
}
}
Problems:
β οΈ Can't test without HTTP request
β οΈ Can't reuse logic in CLI commands
β οΈ Business rules scattered everywhere
β οΈ Changing email library breaks the controller
β After (Clean Architecture)
// Controller: just input/output
final class CreateOrderController
{
public function __invoke(Request $req, CreateOrderHandler $handler): JsonResponse
{
return $this->json(
$handler(new CreateOrderCommand($req->get('email'), $req->get('items')))
);
}
}
// Handler: orchestration
final class CreateOrderHandler
{
public function __invoke(CreateOrderCommand $cmd): OrderResponse
{
$order = Order::create(
Email::fromString($cmd->email),
$this->calculator->calculateTotal($cmd->items)
);
$this->repo->save($order);
$this->eventBus->dispatch(new OrderCreated($order->getId()));
return OrderResponse::fromDomain($order);
}
}
// Domain: pure business logic
final class Order
{
public static function create(Email $email, Money $total): self
{
if ($total->isNegative()) {
throw new InvalidOrderException('Total cannot be negative');
}
return new self(OrderId::generate(), $email, $total);
}
}
Benefits:
β
Test Handler without HTTP
β
Reuse in CLI: php bin/console order:create
β
Business rules in one place (Domain)
β
Swap Doctrine for MongoDB? Change only Infrastructure
βββββββββββββββββββββββββββββ
π When NOT to Use Clean Architecture?
Let me be honest: Clean Architecture is always worth it. But not every client pays for quality.
π― Skip it if:
- Prototype with 2-week lifespan
- Client refuses to pay for proper architecture
- Solo hackathon project
π₯ Use it if:
- Project will live 1+ years
- Team has 2+ developers
- Business logic is complex
- You care about your sanity
π¬ "Clean Architecture is like brushing teeth. Skipping once won't kill you. Skipping always will."
βββββββββββββββββββββββββββββ
π Ready-to-Use Folder Structure
src/BoundedContext/OrderProcessing/
βββ Domain/
β βββ Entity/
β β βββ Order.php
β βββ ValueObject/
β β βββ Money.php
β β βββ Email.php
β β βββ OrderId.php
β βββ Repository/
β β βββ OrderRepositoryInterface.php
β βββ Event/
β βββ OrderCreated.php
β
βββ Application/
β βββ UseCase/
β βββ CreateOrder/
β β βββ CreateOrderCommand.php
β β βββ CreateOrderHandler.php
β β βββ CreateOrderResponse.php
β βββ GetOrder/
β βββ GetOrderQuery.php
β βββ GetOrderHandler.php
β
βββ Infrastructure/
βββ Repository/
β βββ DoctrineOrderRepository.php
βββ Mapper/
β βββ OrderMapper.php
βββ Presentation/
βββ Controller/
βββ CreateOrderController.php
βββββββββββββββββββββββββββββ
π Action Plan: Start Today
Week 1: Create Domain folder, move entities
Week 2: Extract business logic from controllers
Week 3: Create Use Case handlers
Week 4: Introduce Repository interfaces
Don't refactor everything at once! New features β new architecture. Old code can wait.
βββββββββββββββββββββββββββββ
π¬ Your Turn
What's stopping you from implementing Clean Architecture? Is it time, team pushback, or "legacy code too big"?
Share your biggest architecture pain point in the comments! π
Found this useful? Follow for more software architecture deep dives.
Top comments (0)