When I started building web apps with PHP and Laravel, my main goal was simple: make it work.
But as projects grew and legacy code piled up, I hit the wall. The codebase became hard to change, fragile, and painful to test. That’s when I discovered Clean Architecture—and it changed how I think about software.
🧠 What is Clean Architecture?
Clean Architecture is about separating concerns in your application so that:
- The core business logic is independent of frameworks and tools.
- Your app becomes easier to test, maintain, and evolve.
- You can plug and replace things (like Laravel, databases, external APIs) without touching your core logic.
🧱 The Layers and The Golden Rule
Here’s a simplified view of the layers, inspired by Hexagonal Architecture:
- Domain Layer: Entities, business rules. The heart of your application.
- Application Layer: Use cases that orchestrate the domain objects.
- Infrastructure Layer: Frameworks (Laravel), databases (MySQL), APIs, queues, etc.
- Interface/Presentation Layer: Controllers, CLI Commands, anything that presents data to the user. The most important rule is The Dependency Rule: source code dependencies can only point inwards. The Domain layer knows nothing about the Application layer, and neither of them knows anything about Laravel or your database.
🛠️ A Real-World Laravel Example (with Code!)
Say you’re building an e-commerce platform. Instead of putting all the logic in a controller, let's create an order.
The old way: a "fat" controller doing everything.
The clean way: the controller is just a delivery mechanism.
- The Controller (Interface Layer) It only validates input, creates a simple Data Transfer Object (DTO), and calls the use case. It knows nothing about how the order is created.
// app/Http/Controllers/OrderController.php
class OrderController extends Controller
{
public function __construct(private CreateOrderAction $createOrderAction) {}
public function store(StoreOrderRequest $request)
{
$dto = new CreateOrderDTO(
customerId: $request->input('customer_id'),
products: $request->input('products')
);
$this->createOrderAction->execute($dto); // Call the use case
return response()->json(['message' => 'Order created!'], 201);
}
}
- The Action/Use Case (Application Layer) This is where the magic happens. It orchestrates the business logic. Notice it depends on an interface, not a concrete Eloquent model.
// app/Application/Orders/CreateOrderAction.php
class CreateOrderAction
{
public function __construct(private OrderRepository $orderRepository) {}
public function execute(CreateOrderDTO $dto): Order
{
// 1. Core business logic here (e.g., calculate total, check stock)
$order = Order::create(
customerId: $dto->customerId,
totalPrice: $this->calculatePrice($dto->products)
);
// 2. Persist using the repository
$this->orderRepository->save($order);
// 3. (Optional) Dispatch domain event
// dispatch(new OrderCreated($order->id));
return $order;
}
private function calculatePrice(array $products): float
{
// ...logic to calculate price...
return 100.00;
}
}
- The Repository Interface (Application Layer) This is the "port". Our application layer only knows about this contract.
// app/Domain/Orders/Repositories/OrderRepository.php
interface OrderRepository
{
public function save(Order $order): void;
}
This means your core logic has zero dependency on Laravel's Eloquent or MySQL. You could swap it for Doctrine or a NoSQL database by just creating a new implementation of OrderRepository.
✅ Why It Works
Since adopting this approach, I’ve noticed:
- Fewer bugs when refactoring.
- Faster onboarding for new team members (the business logic is easy to find).
- Better test coverage and less reliance on slow database tests.
- Easier migration across frameworks (we did it from Zend to Laravel!).
✨ My Final Tip
Don't try to refactor your entire application at once. Start small. Pick one new feature and try to build it using this approach. You'll learn a lot, and the benefits will become clear very quickly. It's a journey, not a switch.
💬 What about you?
Have you tried Clean or Hexagonal Architecture in your projects?
What's the biggest challenge you've faced when trying to separate concerns?
Let’s share tips and ideas in the comments 👇
Top comments (2)
On high level I keep business logic in service classes, validation in middleware and use repository to communicate with DB. There are some other considerations aa well.
That's a great summary of a solid architecture, thanks for sharing!
I completely agree—using middleware for validation is a great way to keep controllers thin, and separating business logic into services is key.
I'm curious about the 'other considerations' you mentioned. Are you thinking about how you handle things like DTOs, domain events, or something else?