How and Why You Should Adopt Hexagonal Architecture in Your Next Laravel Project

Mateus Cardoso
5 min readJul 8, 2024

--

Imagine yourself in an ancient library, filled with dusty books and wisdom accumulated over the centuries. As you walk through the silent corridors, a particularly intriguing volume catches your attention. On the cover, in elegant golden letters, it reads: “The Developer’s Journey: Exploring Hexagonal Architecture in Laravel”. Intrigued, you open the book and are transported to a world where every architectural decision is a step towards mastery in software development.

Chapter 1: The Legend of Hexagonal Architecture

The story begins in a distant land where developers constantly struggled with monolithic and highly coupled applications. These applications, although powerful, were like dragons difficult to tame, requiring constant maintenance and often failing to keep up with growing demands. It was then that a hero emerged in the form of Alistair Cockburn, who proposed Hexagonal Architecture, also known as “Ports and Adapters”. This architecture promised modularity, testability, and a clear separation of concerns, allowing applications to be as flexible as needed to face any challenge.

Chapter 2: The Encounter with Laravel

You, our modern hero, are already well-equipped with the powerful Laravel framework, known for its elegance and simplicity. However, even Laravel can benefit from the safeguards offered by Hexagonal Architecture. The union of these two worlds can take your development skills to a new level.

Chapter 3: The Battle Plan

To understand how Hexagonal Architecture can be applied to Laravel, we need to draw our battle plan. The architecture is based on three main components:

  1. Domain: The heart of the application, where business logic resides. It is independent of any framework or external technology.
  2. Application: The layer that orchestrates business logic, handling specific use cases.
  3. Infrastructure: Where all external interactions happen, such as databases, external services, and user interfaces.

Chapter 4: Building the Domain

Imagine we are building a library management application. At the center of our domain, we have entities like Book and User. These entities are pure, without external dependencies.

namespace App\Domain;

class Book
{
private $title;
private $author;

public function __construct(string $title, string $author)
{
$this->title = $title;
$this->author = $author;
}

public function getTitle(): string
{
return $this->title;
}

public function getAuthor(): string
{
return $this->author;
}
}

Chapter 5: Defining Interfaces and Services

To ensure separation of concerns and ease of implementation swapping, we use interfaces and services.

Book Repository

namespace App\Domain\Repositories;

use App\Domain\Book;

interface BookRepositoryInterface
{
public function save(Book $book);
public function findByTitle(string $title): ?Book;
}

Book Service

namespace App\Application\Services;

use App\Domain\Book;
use App\Domain\Repositories\BookRepositoryInterface;

class BookService
{
private $bookRepository;

public function __construct(BookRepositoryInterface $bookRepository)
{
$this->bookRepository = $bookRepository;
}

public function lendBook(Book $book)
{
$this->bookRepository->save($book);
}
}

Chapter 6: Orchestrating the Application

The application layer is responsible for specific use cases, such as LendBook or ReturnBook. These use cases know nothing about how data is stored, only how business logic should be applied.

namespace App\Application;

use App\Domain\Book;
use App\Domain\User;

class LendBook
{
public function execute(Book $book, User $user)
{
// Logic to lend the book to the user
}
}

Chapter 7: Connecting the Infrastructure

Finally, we have the infrastructure layer, where Laravel shines. Here, we can use Eloquent to persist data, but in a way that keeps our domain decoupled.

Eloquent Book Repository

namespace App\Infrastructure\Repositories;

use App\Domain\Book;
use App\Domain\Repositories\BookRepositoryInterface;
use App\Infrastructure\Models\BookModel;

class EloquentBookRepository implements BookRepositoryInterface
{
public function save(Book $book)
{
// Code to save the book using Eloquent
BookModel::create([
'title' => $book->getTitle(),
'author' => $book->getAuthor(),
]);
}

public function findByTitle(string $title): ?Book
{
// Code to find a book by title using Eloquent
$bookModel = BookModel::where('title', $title)->first();

if (!$bookModel) {
return null;
}

return new Book($bookModel->title, $bookModel->author);
}
}

Chapter 8: Other Decoupled Services

The same decoupling approach should be applied to all other services your application uses. This includes notification services, external API integrations, payment processing, and any other external interaction. By defining clear interfaces and implementing those interfaces in the infrastructure layer, you ensure that your business logic remains decoupled and testable.

Notification Service

Notification Interface

namespace App\Domain\Services;

interface NotificationServiceInterface
{
public function send(string $message, string $recipient): void;
}

Email Implementation

namespace App\Infrastructure\Services;

use App\Domain\Services\NotificationServiceInterface;
use Illuminate\Support\Facades\Mail;

class EmailNotificationService implements NotificationServiceInterface
{
public function send(string $message, string $recipient): void
{
Mail::to($recipient)->send(new \App\Mail\NotificationMail($message));
}
}

External API Service

External API Interface

namespace App\Domain\Services;

interface ExternalApiServiceInterface
{
public function fetchData(): array;
}

Guzzle Implementation

namespace App\Infrastructure\Services;

use App\Domain\Services\ExternalApiServiceInterface;
use GuzzleHttp\Client;

class GuzzleExternalApiService implements ExternalApiServiceInterface
{
private $client;

public function __construct()
{
$this->client = new Client(['base_uri' => 'https://api.example.com']);
}

public function fetchData(): array
{
$response = $this->client->get('/data');
return json_decode($response->getBody()->getContents(), true);
}
}

Applying the Pattern to All Services

Now that we’ve seen examples of repositories, notification services, and external API integrations, the next step is to apply the same pattern to all other services your application uses. By following this approach, you ensure that your business logic remains decoupled from the infrastructure and that any component can be easily replaced or upgraded without affecting the rest of the application.

Chapter 9: Registering Dependencies

To ensure that our interfaces are resolved to the correct implementations, we configure the bindings in Laravel’s Service Provider.

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Domain\Repositories\BookRepositoryInterface;
use App\Infrastructure\Repositories\EloquentBookRepository;
use App\Domain\Services\NotificationServiceInterface;
use App\Infrastructure\Services\EmailNotificationService;
use App\Domain\Services\ExternalApiServiceInterface;
use App\Infrastructure\Services\GuzzleExternalApiService;

class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(BookRepositoryInterface::class, EloquentBookRepository::class);
$this->app->bind(NotificationServiceInterface::class, EmailNotificationService::class);
$this->app->bind(ExternalApiServiceInterface::class, GuzzleExternalApiService::class);
}

public function boot()
{
//
}
}

Chapter 10: Implementing in Controllers and Models

Controllers

In controllers, you can inject the necessary dependencies for application services. This keeps the controllers simple and focused on control logic, leaving business logic to the services.

namespace App\Http\Controllers;

use App\Application\Services\BookService;
use App\Domain\Book;
use Illuminate\Http\Request;

class BookController extends Controller
{
private $bookService;

public function __construct(BookService $bookService)
{
$this->bookService = $bookService;
}

public function lend(Request $request)
{
$book = new Book($request->input('title'), $request->input('author'));
$this->bookService->lendBook($book);

return response()->json(['message' => 'Book lent successfully!']);
}
}

Models

Models in Laravel can continue to be used primarily as Eloquent Models in the infrastructure layer. They represent the database structure and can be used in repository implementations.

namespace App\Infrastructure\Models;

use Illuminate\Database\Eloquent\Model;

class BookModel extends Model
{
protected $table = 'books';

protected $fillable = ['title', 'author'];
}

Advantages of Hexagonal Architecture

Adopting Hexagonal Architecture in your next Laravel project offers numerous benefits. This approach does not add significant additional complexity to development for those already accustomed to Laravel’s own pattern. However, it allows for several future advantages, such as:

  1. Testability: With business logic decoupled from infrastructure, it is easier to test components individually.
  2. Maintainability: Clear separation of concerns facilitates application maintenance and evolution.
  3. Flexibility: Changes in infrastructure, such as swapping a notification service or an external API, can be made without affecting business logic.
  4. Scalability: Modularity allows for scaling specific parts of the application as needed.

As you close the book, you realize that the journey to mastering Hexagonal Architecture is just beginning. With Laravel by your side and the wisdom of robust architecture, you are ready to face any dragon that comes your way in development.

Epilogue: A New Era of Development

Now, equipped with the knowledge of Hexagonal Architecture, you are ready to transform your Laravel projects into architectural masterpieces. Remember, each line of code is a step in your epic journey as a developer. May your applications always be flexible, robust, and ready for any challenge.

And so, our story continues, with you in the leading role, writing the next chapter in the history of software development.

--

--