Symfony and Hexagonal Architecture

I have been working on many Symfony projects in my career and one of the most common problems where customers call our company is that their software is blocked in the old framework version, or they are not maintainable because it costs a lot to find and fix bugs.

Usually, I try to understand well why those legacy projects are in that state. And often I have found a common pattern: the team at the beginning of the project needs to create an application from zero in a rapid way because there is a strict deadline.
Usually, they start in this way:

  • Install Symfony skeleton project with composer
  • remove demo code
  • auto-generate entities
  • auto-generate controllers
  • ready to develop the application

These steps for me are not the best practice because they started immediately to code something instead of understanding the domain and behaviors.
I think that in the situation explained previously they were guided from the framework.
In my opinion, it is better to concentrate your effort on the domain and you need to treat Symfony (or a framework in general) as a tool not the main core of the software because the real value of your software is the domain, the solution that you implement to solve problems.

Be guided from the framework has many side effects and one of the most dangerous is coupling domain and framework that can create many problems like:

  • impossible to upgrade framework and vendors
  • cost of maintenance, because every bug or new features takes a lot of time to be completed
  • developers are not motivated because the stack it’s very old for the reason of point one
  • not maintainable application
  • a lot of technical debt

But don’t be scared, there is an architecture that can help you to avoid these problems: hexagonal architecture.

Hexagonal architecture history

The hexagonal architecture divides a system into several loosely-coupled interchangeable components, such as the application core, the database, the user interface, test scripts, and interfaces with other systems. This approach is an alternative to the traditional layered architecture. (Wikipedia)

When I read and explain this definition many times developers ask me: is it an over-engineer strategy?

Well, you have more classes, more concepts, and more moments where you need to think a lot about the correct position of a class, naming a class, or a better name for a variable; again it depends on you, I can only recommend to try to apply this strategy and improve your skill with it.

Real-life problems

With hexagonal architecture, you can separate framework and domain so you can upgrade vendors and framework touching a little specific part of your code and not the business logic.
To separate framework and domain, I mean in practice to split them into different directories. I will treat it in a moment.

Another good example of a coupling code is when you have remote services and they change something.

Let’s imagine that you have a payment gateway provider that releases a new version of it and your current version used in your application is not already supported.
You can switch to a new version or replace it with another gateway provider but you know that you have to refactor many parts all over the project because your domain is strictly coupled with the library or service.
So you need to put a lot of effort into rewriting many parts and you can introduce bugs.

With hexagonal architecture, you can replace, and change only adapters, without touching your domain logic because it is decoupled from the framework.

Example of coupled code:


class Payment {
public function pay(Request $request): void
{
$gateway = new YourBankGateway();
$gateway->pay($request->get(‘amount’)));
}
}

In the previous class there are some problems in my opinion:

  • You are calling the method pay with a Request object that represents an HTTP Web request. This means that you can’t call this method from a CLI command, if you need it, you have to duplicate this code or change something.
  • Instantiating the service YourBankGateway inside the method means that if you would like to replace that service with another one, you need to change it all over the code base all lines like that.

Let's try to decouple that code


interface GatewayProvider {
public function pay(Money $amount): void
}
class YourBankGateway implements GatewayProvider {
public function pay(Money $amount): void
{
//do stuff..
}
}
class Payment {
private GatewayProvider $gateway;
public function __construct(GatewayProvider $gateway)
{
$this->gateway = $gateway;
}
public function payThroughGateway(Money $amount): void
{
$this->gateway->pay($amount));
}
}

In this case, and many others, the use of interfaces and dependency injection pattern allow developers to decouple the code because whenever you want you can change the implementation with a new one that implements that interface.

Another advantage of the decoupling example is: now you can call the class Payment from an HTTP web request or CLI command because you need to pass an object Money (usually I try to pass a typed object or DTO) instead of a Request object.

The coupling domain and framework have the dark side effect of creating a not maintainable application.

Maintainable application

A maintainable application is one that increases technical debt at the slowest rate we can feasibly achieve.

What are the measures of a highly maintainable application?

  • Changes in one part of an application should affect as few other places as possible
  • Adding features shouldn’t require to touch any part of the code-base
  • Adding new ways to interact with the application should require as few changes as possible
  • Debugging should require as few workarounds
  • Testing should be relatively easy

To touch less code as possible for new features or legacy features it’s important to delegate a specific class that has one single responsibility.

Single responsibility

  • All things related to the framework
  • All things related to the domain logic
  • All things related to APIs call

So we can create the most important distinction in our project: Domain, Application, and infrastructure.

For domain I mean:

  • entities: models, value objects, and aggregates…
  • interfaces for boundary objects

For application I mean:

  • use cases (application services)

For infrastructure I mean

  • framework
  • implementations for boundary objects
  • controllers, CLI commands

Why a Hexagon?

Ports

There is a port for every way a use case of the application can be invoked (through the UI, API, etc.) as well as for all the ways data leaves the application (persistence, notifications to other systems, etc…). Cockburn calls these primary and secondary ports or usually, developers call them input and output ports.

Primary and secondary are the distinction between intention for communication and the supporting implementation.

Example of port:

interface ProductRepositoryInterface
{
public function find(ProductId $id): ?Product;
}

Ports are only definitions of what we would like to do. They are not saying how to achieve them.

Adapters

They are very concrete and contain low-level code, and are by definition decoupled from their ports.

Example of the adapter:

public class MysqlProductRepository implements ProductRepositoryInterface
{
private $repository; public function __construct(ProductRepository $repository)
{
$this->repository = $repository;
}
public function find(ProductId $id): ?Product
{
return $this->repository->find(id);
}
}

Let's try to represent our ports and adapters inside a real system

Image for post
Image for post

As you can see we have CLI command or HTTP Request that are calling our input adapters inside the infrastructure layer. The Adapters implement our input ports inside the domain layer.
On the other side, we have our output adapters inside the infrastructure layer, which are implementing our output ports inside the domain and can interact with an external system like a database.

So in our PHP Application, we can have a structure like this:

Image for post
Image for post

In this example, you have two different contexts: Payment and Cart.
Under each context, in this example, there is a distinction between domain, application, and infrastructure. It's not mandatory to have all these directories, sometimes could not exists application layer or infrastructure layer.

In your domain, you have your domain logic without reference to any vendors (not always true, for example usually in my domain I use Ramsey/UUID).
Inside this folder, you have also all the ports to specify how you want that data using objects.

In your application folder, you can have services and use cases.

In your infrastructure folder you can have framework code and adapters, so the implementation of domain ports using vendors and technology that you prefer.

Dependency inversion principle

Image for post
Image for post

So an infrastructure class can depend on an application class and domain class.
An application class can depend on a domain class but can’t depend on an infrastructure class.

A domain class can’t depend on an infrastructure or application class.

Advantages of using hexagonal architecture

  • Separating the domain from infrastructure increases testability because many parts of the code don’t need a database connection, internet connection, or filesystem. You can create a lot of unit tests.
  • You can replace an adapter without affecting the ports, you can change the database and the domain doesn’t need to change.
  • You can postpone the choice of vendors, databases, servers, etc… because it’s more important to model your domain, so you can have more knowledge when you need to make that choice.
  • You can update vendors and frameworks without touching your domain code.

When to use it

How about legacy code without hexagonal architecture?

It starts with creating new directories like Infrastructure and domain.

Now new concepts and features could be developed into those directories.

With old features, if it’s possible and small I try to create pull requests to migrate little concepts with the new architecture.
When I migrate an old legacy piece of code I try to follow a golden rule that I love the boy scout rule:

Leave your code better than you found it.

Let’s improve again our projects

  • DDD (Domain-driven design)
  • CQRS pattern (Command Query Responsibility Segregation)
  • Event sourcing
  • TDD
  • BDD

All these concepts methodologies and approaches can improve again your projects.

Software developer @Flowing

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store