Laravel’s ‘batteries-included’ philosophy is a double-edged sword. While it accelerates initial development, it often leads to tight coupling where business logic is inextricably woven into Eloquent models, controllers, and service providers. This coupling creates a ‘Framework-Centric’ [[architecture](/blog/architecture-decision-records-capturing-engineering-decisions-that-outlast-the-team)](/blog/clean-architecture-in-flutter-with-bloc-a-practical-guide) that becomes increasingly difficult to maintain as the application grows in complexity.
Hexagonal Architecture, or the Ports and Adapters pattern, aims to solve this by placing the domain logic at the center of the application, isolated from external concerns like databases, HTTP requests, or third-party APIs. By treating the framework as an implementation detail, we gain the ability to test business rules in isolation and swap infrastructure components without modifying the core.
Defining the Domain Core
The core of your application should be pure PHP, free from any dependencies on the Illuminate namespace. This layer contains your Domain Entities, Value Objects, and Domain Services. It defines the business rules, not the database schema.
namespace Domain\Order;
class Order {
public function __construct(
private OrderId $id,
private Money $total,
private OrderStatus $status
) {}
public function complete(): void {
$this->status = OrderStatus::COMPLETED;
}
}
Ports: The Interface to the Outside
Ports define the contracts that the domain requires from the outside world. In PHP, these are typically represented by interfaces. If your domain needs to persist an order, it doesn’t know about Eloquent; it knows about an OrderRepositoryInterface.
namespace Domain\Order;
interface OrderRepositoryInterface {
public function save(Order $order): void;
public function findById(OrderId $id): ?Order;
}
Adapters: Implementing the Contracts
Adapters are the implementation layer. This is where your Eloquent models reside. An ‘Infrastructure’ adapter implements the OrderRepositoryInterface to talk to the database. Because the domain only knows the interface, you can swap this for an in-memory repository during testing.
namespace Infrastructure\Persistence\Eloquent;
use Domain\Order\OrderRepositoryInterface;
class EloquentOrderRepository implements OrderRepositoryInterface {
public function save(Order $order): void {
OrderModel::updateOrCreate(['id' => $order->id()], [...]);
}
}
Dependency Inversion in Laravel
To glue these together, utilize Laravel’s Service Container. In your AppServiceProvider, you bind the interface to the concrete infrastructure implementation. This preserves the decoupling; your business logic never instantiates the repository directly.
$this->app->bind(
\Domain\Order\OrderRepositoryInterface::class,
\Infrastructure\Persistence\Eloquent\EloquentOrderRepository::class
);
Trade-offs and Considerations
- Increased boilerplate: You will write more classes and interfaces, which can feel verbose for simple CRUD apps.
- Cognitive load: Developers must maintain a clear mental model of layer boundaries.
- Improved testability: Unit tests for business logic become lightning fast as they no longer require a database connection.
- Flexibility: You can migrate from Eloquent to a different persistence layer or even a microservice without touching the domain logic.
Conclusion
Hexagonal Architecture is not a mandate for every Laravel project; it is a strategic choice for systems where domain complexity is high and longevity is a primary requirement. By strictly enforcing boundaries between the core business rules and the framework, you create a system that is resilient to change and easier to evolve over time.