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)