SYS://VISION.ACTIVE
VIEWPORT.01
LAT 28.0222° N
SIGNAL.NOMINAL
VISION Loading
Back to Blog

Clean Architecture in Laravel: Building Maintainable Applications

Shane Barron

Shane Barron

Laravel Developer & AI Integration Specialist

Why Clean Architecture?

After building systems for 40 years, I've seen codebases that started clean become unmaintainable within a year. Clean architecture isn't about following rules—it's about making systems that remain adaptable as requirements change and teams grow.

The core principle: business logic should be independent of frameworks, databases, and external services. Your domain shouldn't know it's running in Laravel.

The Dependency Rule

Dependencies point inward. Outer layers depend on inner layers, never the reverse.

  • Entities: Core business objects and rules
  • Use Cases: Application-specific business rules
  • Interface Adapters: Controllers, presenters, gateways
  • Frameworks: Laravel, Eloquent, external services

Implementing in Laravel

Domain Layer

// app/Domain/Orders/Order.php
namespace App\Domain\Orders;

class Order
{
    public function __construct(
        public readonly string $id,
        public readonly CustomerId $customerId,
        public readonly Money $total,
        public readonly OrderStatus $status,
        public readonly array $items
    ) {}

    public function canBeShipped(): bool
    {
        return $this->status === OrderStatus::Paid
            && $this->hasValidShippingAddress();
    }

    public function ship(TrackingNumber $tracking): self
    {
        if (!$this->canBeShipped()) {
            throw new CannotShipOrderException($this);
        }

        return new self(
            $this->id,
            $this->customerId,
            $this->total,
            OrderStatus::Shipped,
            $this->items
        );
    }
}

Use Cases

// app/Application/Orders/ShipOrder.php
namespace App\Application\Orders;

class ShipOrder
{
    public function __construct(
        private OrderRepositoryInterface $orders,
        private ShippingServiceInterface $shipping,
        private NotificationServiceInterface $notifications
    ) {}

    public function execute(ShipOrderRequest $request): ShipOrderResponse
    {
        $order = $this->orders->findById($request->orderId);

        if (!$order) {
            throw new OrderNotFoundException($request->orderId);
        }

        $tracking = $this->shipping->createShipment($order);
        $shippedOrder = $order->ship($tracking);

        $this->orders->save($shippedOrder);
        $this->notifications->orderShipped($shippedOrder);

        return new ShipOrderResponse($shippedOrder, $tracking);
    }
}

Interface Adapters

// app/Infrastructure/Repositories/EloquentOrderRepository.php
namespace App\Infrastructure\Repositories;

class EloquentOrderRepository implements OrderRepositoryInterface
{
    public function findById(string $id): ?Order
    {
        $model = OrderModel::find($id);

        if (!$model) {
            return null;
        }

        return $this->toDomain($model);
    }

    public function save(Order $order): void
    {
        OrderModel::updateOrCreate(
            ['id' => $order->id],
            $this->toDatabase($order)
        );
    }

    private function toDomain(OrderModel $model): Order
    {
        return new Order(
            $model->id,
            new CustomerId($model->customer_id),
            new Money($model->total, $model->currency),
            OrderStatus::from($model->status),
            $this->mapItems($model->items)
        );
    }
}

Testing Benefits

class ShipOrderTest extends TestCase
{
    public function test_ships_paid_order(): void
    {
        $order = OrderFactory::paidOrder();
        $orders = new InMemoryOrderRepository([$order]);
        $shipping = new FakeShippingService();
        $notifications = new FakeNotificationService();

        $useCase = new ShipOrder($orders, $shipping, $notifications);
        $response = $useCase->execute(new ShipOrderRequest($order->id));

        $this->assertTrue($response->order->status === OrderStatus::Shipped);
        $this->assertNotNull($response->tracking);
    }
}

When to Apply

Clean architecture adds complexity. Use it for:

  • Long-lived applications
  • Complex business logic
  • Multiple teams working on the same codebase
  • Applications that might change frameworks or databases

Conclusion

Clean architecture keeps your business logic independent and testable. The initial investment pays off in maintainability and flexibility over time.

Share this article
Shane Barron

Shane Barron

Strategic Technology Architect with 40 years of experience building production systems. Specializing in Laravel, AI integration, and enterprise architecture.

Need Help With Your Project?

I respond to all inquiries within 24 hours. Let's discuss how I can help build your production-ready system.

Get In Touch