Hexagonal Architecture, also known as Architecture of Ports and Adapters, is one of the most used architectures nowadays. The aim of this article is to list down the main characteristics, applying hexagonal architecture to a Symfony project. 

 

Applying Hexagonal Architecture: main principles

Hexagonal Architecture is a type of architecture that is based on the principle of Ports and Adapters. This architecture follows the premise of guaranteeing the total abstraction of the domain with respect to all the dependencies we may have (repositories, frameworks, third-party libraries, etc.).

The principle of Ports and Adapters, as its name suggests, is based on complete isolation of our domain from any external dependency and managing the interaction of these dependencies with our domain by using:

  • Ports: that allow us to standardize the functionalities/actions that we use from these dependencies and in what way or with what data structures they will do it.
  • Adapters: that will be responsible for adapting the dependencies to the data structures that we need within our domain.

In addition to these basic principles, hexagonal architecture follows the principles defined by Robert C. Martin in The Clean Architecture to ensure a clean, scalable and adaptable architecture to change:

  • Independence of the framework: As we have defined previously, the hexagonal architecture guarantees total independence of any framework or external dependence.
  • Testable: By having the business rules isolated in our domain, we can unitarily test all the requirements of our application without any external factor that could adulterate the functionality of this one. 
  • Independent of the UI: The user interface is a continuously changing ecosystem, which does not mean that the business rules do it equally. UI independence allows us to maintain a more stable domain, avoiding having to enter to modify it by completely external factors to the business rules.
  • Independent of the database: Our domain should not know the way we decided to structure or store our data in a repository. It doesn’t influence it, except coupling and a future problem if we decide to change our database at some point.
  • Independent of any external agent: Our domain is a representation of our business rules, therefore it does not concern the knowledge of any external agency. Each external library will have its own adapter, which will be responsible for the interaction with the domain.

The Hexagonal Architecture is part of the so-called Clean Architectures, which are based on the following scheme:

hexagonalHexagonal Architecture Scheme. Taken from Novoda

 

We can see that the concepts explained above fit perfectly into this scheme. And in addition to applying these principles, in our practical example we will always try to apply the SOLID principles, in the best possible way. If you want to know more about Hexagonal Architecture you can find more information in the blog of Apiumhub.

 

Practical exercise: applying Hexagonal Architecture

We will perform a simple exercise that allows us to see in a practical way, the application of the concepts named above. Let’s imagine that we have to develop an application for a warehouse in PHP 7 using Symfony 3. In this first article, we will develop the introduction of products in the system.

To do this we will develop a POST-type access point that we will define as “/ products”.

As we can see, the statement can already give us some clues of the clearest external dependencies that we will need:

  • Symfony, as an application framework
  • A database to store the introduced products: In this case, we will use Doctrine as an ORM to interact with a MySQL

In this exercise we are going to apply an inside-out strategy, therefore, let’s start by defining our domain. We are going to implement a very basic Product entity with four fields: id, name, reference, and date of creation.

To ensure that the data that will pass through our domain is as expected, we have declared our entity with a private constructor and we can only instantiate our entity through the static function fromDto waiting for a specific data structure based on a Data Transfer Object that we have defined as CreateProductRequestDto, as we can see below:


class Product
 {
     private $id;
 
     private $name;
 
     private $reference;
 
     private $createdAt;
 
     private function __construct(
         string $name,
         string $reference
     )
     {
         $this->name = $name;
         $this->reference = $reference;
         $this->createdAt = new \DateTime();
     }
 
     public static function fromDto(CreateProductRequestDto $createProductResponseDto): Product
     {
         return new Product(
             $createProductResponseDto->name(),
             $createProductResponseDto->reference()
         );
     }
 
     public function toDto()
     {
         return new CreateProductResponseDto(
             $this->id,
             $this->name, $this->reference, $this->createdAt
         );
     }
 }

As we can see, the parameters of our entity are private, and will only be accessible to the outside through the data structure defined as CreateProductResponseDto, thus ensuring that we are only going to expose the data that we really want to show. With this methodology, we wanted to go one step further, and we are going to differentiate between the layer named as domain and the application layer, which often tend to mix, and we are only going to allow interaction with our domain if the structure of data is adequate.

Next, we will proceed to implement the interaction of a database with our domain. To do this, we will need to define a port that defines the functionalities that our domain can perform with the external dependency and an adapter that implements the relationship between the external dependency and our domain.

In this case, as a port, we will define a repository interface in which we will define the methods that we will need from our domain. For this we have created the ProductRepositoryInterface class, which will perform the port function:


interface ProductRepositoryInterface
{
    public function find(string $id): Product;

    public function save (Product $product): void;
}


And as an adapter we have created the ProductRepositoryDoctrineAdapter class that will implement the functions defined in the previous interface. These will be impure functions, since they interact directly with the external dependency and adapt the data to be used in our domain, for example:


class ProductRepositoryDoctrineAdapter implements ProductRepositoryInterface
{
    /** @var EntityRepository */
    private $productRepository;

    /** @var EntityManager */
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->productRepository = $entityManager->getRepository(Product::class);;
    }

    public function find(string $id): Product
    {
        $this->productRepository->find($id);
    }

    public function save(Product $product): void
    {
        $this->entityManager->persist($product);
        $this->entityManager->flush();
    }
}

In this case, we can see how the external dependency is injected into the constructor, and how the functions defined in the interface that we use as a port are implemented.

Now let’s see how all this will interact with our domain. For this, we have created an application layer, in which we will have the actions that our system can perform. In this layer also, it will be where we will introduce the calls to the adapters, to which we will have access by means of dependency injection


class ProductApplicationService
{
    private $productRepository;

    public function __construct(ProductRepositoryInterface $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    (...)
}

As we can see, in the constructor we have defined the port ProductRepositoryInterface and not the adapter, why? Very simple, if in the future instead of using Doctrine with MySql we just want to persist our data in a Redis cache, for example, it would be as simple as creating a new adapter that complies with the contract defined in ProductRepositoryInterface and change the injection of the dependency that we have defined in a yml file:


services:
  product.application.product_service:
    class: ProductBundle\Application\ProductApplicationService
    arguments: ['@product.repository.product.doctrine_adapter']

  product.repository.product.doctrine_adapter:
    class: ProductBundle\Repository\ProductRepositoryDoctrineAdapter
    arguments: ['@doctrine.orm.entity_manager']

It will be as simple as declaring the new adapter, and replacing the dependency of the application service. This shows us that we have built a completely agnostic domain of its dependencies, since we will not have to touch a single line of productive code to change the dependency.

Now let’s see the use of the adapter in the application service:


class ProductApplicationService
{
    (...)

    public function createProduct(
        CreateProductRequestDto $createProductRequestDto
    ): CreateProductResponseDto {
            $product = Product::fromDto($createProductRequestDto);
    
            $this->productRepository->save($product);
    
            return $product->toDto();
        }
}

As can be seen, in the application service we are conducting the orchestration between the pure domain (the creation of a domain entity, based on the data structure CreateProductRequestDto) and the call to the adapter to perform the persistence of the created object. In this way we guarantee that the domain layer will never become contaminated or interact with adapters of external dependencies, thus focusing only on the business rules that the application has.

As a last point and as you may have noticed previously, we have created two Data Transfer Objects (DTOs): CreateProductRequestDto and CreateProductResponseDto. This is to ensure the correct structure of the input and output data of our domain. If we force a specific DTO to be sent to us from the controller, we are abstracting from how the data is being sent to our application and guaranteeing that they enter our domain with the appropriate data structure. The same happens with the CreateProductResponseDto, which assures us a centralized way of deciding which data we expose from our domain abroad.

If we try to extrapolate this practical exercise, applying Hexagonal Architecture,  to the image that appears in the previous section, we can see how the definition of our layers fits perfectly into the theoretical scheme: 

Structure of our example applied to the Hexagonal Architecture scheme.

 

Conclusions: applying Hexagonal Architecture

Reviewing the concepts that we have enumerated in the first part of our article together with the developed example, these are the outcomes of the application of these concepts in our application:

  • The persistence of our domain data has been completely abstracted
  • An example of a port and an adapter has been successfully implemented
  • DTOs have been implemented to see an example of how to transfer the data between the different layers of abstraction (Controller and Application layer, which separates our domain from the framework).
  • The separation by layers gives us a system that is easily tested in a unitary way, allowing us to mock the dependencies of our application service.
  • A completely agnostic domain of the dependencies, the framework and the UI has been created.

I am aware that this has been a very simple example of applying Hexagonal Architecture, but I wanted to emphasize the development of the architecture without getting lost in the details of implementation of the application itself. We have to understand the theories applied in this exercise and what they bring to our day-to-day lives as developers, helping us to implement clean, maintainable and easily testable code.

If you are working on a Symfony project and you need software development help, let us know! We would be happy to know more about your project! 

 

And if you would like to know more about applying Hexagonal Architecture, I highly recommend you to subscribe to our monthly newsletter by clicking here.  

 

And if you found this article about applying Hexagonal Architecture to a Symfony project interesting, you might like…

 

Scala generics I: Scala type bounds

Scala generics II: covariance and contravariance  

Scala generics III: Generalized type constraints

BDD: user interface testing

F-bound over a generic type in Scala

Microservices vs Monolithic architecture

“Almost-infinit” scalability

iOS Objective-C app: sucessful case study

Mobile app development trends of the year

Banco Falabella wearable case study 

Mobile development projects 

Viper architecture advantages for iOS apps 

Why Kotlin ? 

Software architecture meetups

Pure MVP in Android 

Be more functional in Java ith Vavr