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 was invented by Alistair Cockburn in an attempt to avoid known structural pitfalls in object-oriented software design, such as undesired dependencies between layers and contamination of user interface code with business logic, and published in 2005.
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
A project written 10 years ago is blocked into an old PHP version and you would like to migrate to a new version.
Upgrade PHP means that you need to upgrade the framework and vendors touching the business logic because everything is coupled.
You can’t upgrade in a securly way because the code is not fully covered by tests.
In that case, you have a not maintainable application.
All these problems are common if you have coupled your domain and framework.
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
For maintainability I mean is the absence (reduction) of technical debt.
Technical debt is the debt we pay for our (bad) decisions, and it’s paid back in time and frustration.
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
A good concept to follow is the single responsibility for the code but it exists also for the architecture: what changes for the same reason should be grouped, for examples:
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?
The number of sides is arbitrary.
The point is that it has many sides.
Each side represents a “port” into or out of our application.
Each port can be used by adapters to make our system work fine.
Let’s explain what ports and adapters mean in depth.
Ports
Ports are like contracts so they will not have any representation in the codebase.
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
Adapters are the implementation of the ports because for each of these abstract ports we need some code to make the connection work.
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
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:
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
Now If you now combine hexagonal architecture with the dependency inversion principle you can improve again a lot your projects.
The dependency inversion principle means that High-level modules should not depend on low-level modules. Both should depend on abstractions.
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
There are a lot of advantages for me using hexagonal architecture like:
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
At the moment I am trying to use this architecture always because when you start to think with this mindset it’s very difficult to come back.
How about legacy code without hexagonal architecture?
Usually, with a legacy application that doesn’t follow this architecture, I suggest to the team to start to try new things to make the domain and the code better and clear.
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
To improve your domain and your code I can suggest using
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.
Top comments (8)
Thanks for the tips. It's hard to be introduced in Symfony with Hexagonal Architecture, nice job.
I would like to see it in video , have you any source about a little project as a blog with Symfony and Hexagonal architecture ?
Yes, I have made a talk at sfday2020 about it and I hope that in the next weeks I can share it on Twitter.
Excellent article on a subject that is rarely covered !
Thanks a lot ! Don't hesitate to write more ;)
I'll use it right away in my new symfony 5.2 project
Thanks a lot for your feedback, I really appreciate it.
I am thinking to write more about this architecture!
Yes please!
valuable info, thanks Alessandro!
With this, you don't even need a framework. You can do this with just a router and whatever packages your project needs and it will be easy to maintain.
Yes, you could do that, always I think that it depends on the project and the team.
There a lot of different solutions, but the approach is common: protect the domain.