Dig into the rabbit hole of Hexagonal Architecture.
- 1. Introducing
- 2. Bounding Context
- 3. Hexagonal Architecture
- 4. Project Targets
- 5. Creating Domain Layer
- 6. Creating Application Layer
- 7 Testing Application Core
- 8. Creating Infrastructure Layer
- 9 Testing Infrastructure Layer
- 10. Creating UserInterface Layer
- 11. Communicate between Bounded Contexts
- 12. Porting Application Core into another language
- 00. Summary
- 00.1 Thank you
- 00.2 Links
The "Project Rabbit Hole" is a case study by me - Alexander Schranz. I'm currently working as Web Developer for the Open Source Content Management System called Sulu and did begin my career as developer in 2012. Today I'm mostly working with Symfony, Doctrime ORM, Elasticsearch, Redis and ReactJS.
At Sulu we did more and more use in our projects the Hexagonal architecture. This case study should show the advantages of using the Hexagonal architecture. How I'm interpreting it and how it does solve for me the problem of creating long term maintainable software.
The case study focus on creating a reusable library which can be used in different frameworks without the need of changing its core application logic. It should also show where the weak points are and why some frameworks suit better to create sustainable software and why others not.
Before dig into the Hexagonal architecture I want to do a detour to Domain Driven Design (DDD). In there exist the term of Bounding Contexts which is a way to help to split your application into different contexts which should work by their own.
The best example of how a software can be split into multiple context is a shop. A typical shop software can be split into the following context:
- Product
- Order
- Invoice
Every context can work by itself. Example your website does only need to present products on pages. In this case there is no order or invoice needed. So the product is defined as its own bounded context. In other case the order context can exist without invoice because invoicing is done over a third party service.
In this Contexts the Order does just provide an API / Interfaces to create Order Items, but does know nothing about the Product. In this type of context setup it's also easier to add additional thing which can be ordered, in example an own Ticket context.
The contexts a shop would then look like this:
+---------+
| Product |-------+
+---------+ | +-------+ +---------+
+-------+ Order |-------+ Invoice |
+---------+ | +-------| +---------+
| Ticket |-------+
+---------+
We will go deeper into how the contexts can communicate / integrate in the chapters about the Hexagonal architecture.
The hardest thing in the initial project setup is defining the contexts, what does belong into which context. Having to many contexts can make the software hard to maintain and make it hard to understand how things work together. Having to less can make the code also messy and rewrites of specific contexts hard or even impossible in certain time.
About bounded context and DDD have also a look at Martin Fowlers website: https://martinfowler.com/tags/domain%20driven%20design.html about DDD topics.
If creating a library mostly your library is the whole context. In the example code the context is called Event and will implement a simple translatable Event model which can be created, modified, loaded and removed.
The "Hexagonal architecture" also called "Ports and Adapters", is a way of how you can split your context/library into a maintainable and reusable way.
If you are interested in the origin of the hexagonal architecture you should definitely have a look at the Alistair Cockburn first published article about it on his website. Sadly currently only available in the webarchive https://web.archive.org/web/20170730135337/http://alistair.cockburn.us/Hexagonal+architecture.
Most which I'm familiar with is coming from the great Matthias Noback which did write several articles about this topic which can be found on his website https://matthiasnoback.nl/tags/hexagonal%20architecture/.
The architecture is mostly shown the following way:
+--------------------------------------------------------+
| |
| Adapters |
| |
| +----------------------------------+ |
| | | |
| | Ports | |
| | | |
| | +------------------+ | |
| | | | | |
| Adapters | Ports | Application Core | Ports | Adapters |
| | | | | |
| | +------------------+ | |
| | | |
| | Ports | |
| | | |
| +----------------------------------+ |
| |
| Adapters |
| |
+--------------------------------------------------------+
Here we already see why it is called "Ports & Adapters" the Application core communicates over ports with its adapters.
We will use a common Layered Hexagonal Architecture in our code. The main
business logic should in this architecture exist in the Application Core
,
which we will split into the Domain
and the Application
namespace. The
adapters we will split into UserInterface
and Infrastructure
. This layers
are also common without the UserInterface so the UserInterface adapters there
live in the Infrastructure layer. The ports to the adapters should be kept that
the adapters are easily replaceable or new adapters can be added:
| Adapters | Port | Application Core | Port | Adapters |
+----------------------+------+-------------------------------------------------+------+------------------------------+
| | | | | |
+--------------------+
| Domain |
+------------------------------------->| | +-----------------------------+
| | +------------+ | | Infrastructure |
+---------------------+ +------------------------+ | | Model | | | |
| UserInterface | | Application |-->| +------------+ | | +---------------------+ |
| | | | | |<-----| | Doctrine Repository | |
| +------------+ | | +----------------+ | | +------------+ | | +---------------------+ |
| | Controller | |----->| | Message | |-->| | Repository | | | |
| +------------+ | | +----------------+ | | +------------+ | | +---------------------+ |
| | | | | |<-----| | Serializer | |
| +------------+ | | +----------------+ | | +------------+ | | +---------------------+ |
| | Command | |----->| | MessageHandler | |-->| | Exception | | | |
| +------------+ | | +----------------+ | | +------------+ | | +---------------------+ |
+---------------------+ +------------------------+ | |<-----| | Message Bus | |
| A | +------------+ | | +---------------------+ |
| | | | Event | | +-----------------------------+
| | | +------------+ | | A
| | +--------------------+ | |
| | | |
| +---------------------------------------------------+ |
| |
+-------------------------------------------------------------------------------------------+
The Application Core, which includes the Domain
and Application
namespaces,
should have no external dependency when adopting a strict Hexagonal
Architecture.
The Adapters, which live in the UserInterface
and Infrastructure
namespaces,
which are very specific like the Repository implemented with
an ORM like Doctrine lives in the Infrastructure namespace and are
referenced in the Application Core just by a RepositoryInterface and by the
Infrastructure
Code for Symfony Framework correctly injected over
Dependency Injection.
So now we already did talk about the 4 Layers of our Hexagonal Architecture which are:
- Application
- Domain
- Infrastructure
- UserInterface
The Hexagonal Architecture defines a specific way which layers are allowed to access the models and services of other layers. This is shown in the previous graphic with the arrows.
The domain layer is the layer which contains your domain logic and is only allowed to access classes and interface of itself. It provides mostly the following:
Events
Exceptions
Factories
Models
Repository
The models should be defined as rich models, so they should contain the logic for your domains, thrown expected exception for the Application or UserInterface. As Port to other Layers the Domain layer should provide for example an Interface for the Persistence Layer over a RepositoryInterface. The Interface can be used then by an Infrastructure Implementation. This type of pattern is well known as the "Dependency Inversion Principle" (DIP).
The application layer defines the services and application models. It is allowed to access the Domain Layer and itself. In our case mostly the following exist in the Application Layer:
Messages
MessageHandlers
- any other application service provided by your library/project
Actually the Messages and MessageHandlers are in our Setup Commands and CommandHandlers from CommandBus. But as command are in most framework associated with CLI scripts we did decide to go here with Message and MessageHandler how it is done in the Symfony Framework.
In real-world applications you will find yourself quickly in a place of reinventing the wheel. That’s the reason why we allow the usage of some external dependency like PSR EventDispatcher or MessageBusInterface in the Application namespace and Ramsey Uuid in our Domain Models. See also Project Targets.
The infrastructure layer defines your adapters to other libraries like ORMs or to integration of your code into frameworks. The infrastructure is allowed to access the Domain Layer and the Application Layer. In our case mostly the following exist in the Application Layer:
Framework
Integrations- Dependency Injection
ORM
Integrations- Repository
As we need to keep the Domain and Application free from Infrastructure code things like ORM annotations should be replaced with xml files or defining the ORM schema over external code files.
The Hexagonal Architecture targets that such things like an ORM can in future be easy replaceable. As an example replacing Doctrine with Cycle ORM should have no impact on your Business Logic and should so easily be achievable with this architecture. Or another example the service are registered in Symfony correctly providing in the Symfony Infrastructure Namespace a Bundle class or for the Spiral framework providing a Bootloader class.
The userinterface layer as it says is where user input is coming in. The userinterface is allowed to access all other layers so the Domain Layer, Application Layer and Infrastructure Layer. The UserInterface layer contains in our setup:
- Controllers
- Commands (CLI Scripts)
Different frameworks need that there controllers or commands are written in a certain way. See also Project Targets.
The first target of the Project "Rabbit Hole" is to give other developers a great overview about Hexagonal architecture. This should not only be done by this text but also by a whole implemented library which code the reader can analyse. So the developers are not getting lost in the "Rabbit Hole" of Hexagonal architecture.
The second target and personally target is finding a balance between a strict Hexagonal architecture and avoiding reinventing the wheel. This should be done by build on top of widely spread interfaces providing the PSR packages and other well spread library. As example here ramsey/uuid and flysystem. I think this balance is very important to keep the development productive and that the architecture is not a stopper and so drawing the line must be defined for each individual service.
The third target of this implementation is to show which frameworks make it harder to implement its infrastructure / userinterface layers and requires more work and which can easily. Why some patterns don't work and why others are better suited for such libraries.
The fourth target is can user interfaces defined which can be used by different frameworks, so does a framework change maybe also don't need to require that your libraries user interfaces also don't need to be rewritten. And so a library can easily support multiple frameworks and provide so its model and entities for it.
The project is for maintainable creating in a monorepository, the library will exists in the src/event directory with its Hexagonal Architecture. The proof that the library can be used in different framework is done by installing different framework skeletons into the frameworks directory which will over local composer package install the library and use its framework integration.
After this introduction in the theoretical part it is time to go into the practical part.
The domain namespace is responsible for the domain models and services. Common things inside it are Models, Exceptions, Events, Repository (Port), Factories.
The best way of defining the Domain Models is to begin with some kind of class
diagram. This allows you to discuss relations between your models without
the need of actually implementing them. In our example we have a very basic
models with an Event
and an EventTranslation
. Modeling the Domain classes
should help you also define the Bounded Contexts which from
Chapter 2 - Bounding Context.
+-----------------------------------[ Event Context ]---------------------------------+
| |
| +---------------------------------+ +-----------------------+ |
| | Event | | EventTranslation | |
| +---------------------------------+ +-----------------------+ |
| | -id: int | 1 n | -id: int | |
| | -defaultLocale: string |<------------->| -locale: string | |
| +---------------------------------+ | -title: string | |
| | + getDefaultLocale(): string | +-----------------------+ |
| | + getTranslations(): i<ET> | | + getTitle(): string | |
| | ... | | ... | |
| +---------------------------------+ +-----------------------+ |
| |
+-------------------------------------------------------------------------------------+
In which detail you creating this diagrams is always up to you. Now as we have defined our Models we can begin to implement them.
After this we extract the Model public methods into an Interface mostly IDEs are helping here like Intellij/PHPStorm from Jetbrains. After that we should use everywhere where we are expecting a specific Model using the Interface instead of the Model class.
In the next step we should also define in our Domain which Exception could happen. At the begin of a project there will be not much Exception so we have here 2 expected Exceptions:
The Domain Models should not be just some Entities with getter and setter they should implement also some kind of business logic. Mostly here is spoken about Rich Models, which can have complex logic in itself and also throw exceptions when something unexpected happens.
Here a more complex example on a User Model:
class User {
private string $username;
private string $email;
private string $passwordHash;
public function __construct(
string $username,
string $email,
string $rawPassword,
callable $passwordHasher
) {
$this->changeUsernameAndEmail($username, $email);
$this->changePassword($rawPassword, $passwordHasher);
}
public function changeUsernameAndEmail(string $username, string $email): void
{
if (str_contains($username, '@') && $username !== $email) {
throw new \InvalidArgumentException('A username containing the @ symbol must much the given email address.');
}
$this->username = $username;
$this->email = $email;
}
public function changePassword($rawPassword, callable $passwordHasher): void
{
$this->passwordHash = $passwordHasher($rawPassword);
}
}
The advantage of rich models are also that they are valid objects after they
are constructed. So the constructor of your model already defines all required
attributes which the Model needs to be persisted and setters are only used
for optional fields. Also it is common that not every field has a setter and
some can so only be set at the beginning of creating the model, when example
a field is not changeable like our EventTranslation::locale
.
The most common service which exist in the Domain is the ModelRepository. As the Repository should allow to save our Models in a persistence storage we need to create a Port for it. This Port is the RepositoryInterface which uses Dependency Inversion Principle so there is no direct relation to the persistence storage.
Another common service is the Domain is the ModelFactory to create an instance of your model. In our case there exist no ModelFactory as we did directly add a create and createTranslation method to our Repository. This is because of experience that how a model need to be constructed can depend on the used persistence layer.
The Interface will make use of our before defined EventNotFoundException in
case of the getOneBy
is called with none exist filter parameters. If
carefully look at the defined RepositoryInterface you will see that there
are 2 methods which look equal the getOneBy
and findOneBy
. Where in most
cases the getOneBy
where the Model is returned or a EventNotFoundException
is thrown is the best. There are usecases where the findOneBy
make more
sense to make readable code. As an example:
// bad example for using getOneBy
try {
$existUser = $this->userRepository->getOneBy(['username' => $username]);
throw new UsernameAlreadyTakenException($username);
} catch (UserNotFoundException $e) {
// expected program flow
}
$user = new User($username);
// ..
In this case the findOneBy
is a better used the expected program flow is
not going through an exception:
// good example replacing getOneBy with findOneBy
$existUser = $this->userRepository->findOneBy(['username' => $username]);
if ($existUser) {
throw new UsernameAlreadyTakenException($username);
}
$user = new User($username);
// ..
How a RepositoryInterface can be defined is an own topic. I personally prefer that the Interfaces look similar to each other instead of defining too specific methods. Still I would not create a common interface as it should just define the method you really require and inheritance always adds complexity.
With the created RepositoryInterface we did define our minimal domain setup.
The minimal domain setup in the chapter before allows us now to create our application logic. The application layer provides the services for the outside which can be used. In our case we have decided that we want to use a command bus to communicate with our application. Because of conflicts of the name command with CLI command/scripts in widely used frameworks we have decided to go here instead with Command and CommandHandler with Message and MessageHandler.
The messages or commands are at the end Data transfer objects (DTOs), which converts the given data into an accessible format for the handlers:
All data should be injected into the message over the constructor and so the message should be immutable by design. It should only define getter methods to make data easier to access. The data given to the message should just use primitives types, this allows to create it from different contexts (cli/http/...). Also no data should be needed to be parsed out like in arguments of a cli command. This type of things should happen in the UserInterface layer.
The primitives types not only allows to create the message at any case but also make it easier when using like in our case a $data array directly forwarding in example our $_POST data or in whatevery case your framework does provide this kind of data. Example in symfony with the http foundation request data:
$message = new CreateEventMessage($request->request->all());
Where we did in our case created typical create, modify and remove messages.
It is common that there exist more specific message which represent your
domain. Example here could be a ChangePasswordMessage
or a
PublishEventMessage
.
The messages can be validated in different ways. It's possible to validate it directly in the constructor. If an unexpected data is given you can throw a InvalidArgumentException e.g.:
Assert::string($data['title'] ?? null, 'Expected a valid title to be given.');
This is one of the simplest and easiest way for validation. Another way of validating the inputted data is adding a validating method to it. e.g.:
public function validate(): void
{
$errors = [];
if (empty($this->data['title']) || !is_string($this->data['title'])) {
$errors[] = [
'field' => 'title',
'message' => 'Expected a valid title to be given.',
];
}
if (empty($this->data['locale'])
|| !is_string($this->data['locale']) ||
!in_array($this->data['locale'], ['en', 'de'])
) {
$errors[] = [
'field' => 'locale',
'message' => 'Expected locale "en" or "de" given.',
];
}
throw new ValidationException($errors);
}
The advantage of this method is that you validate multiple inputted data and not just one by one. Instead of an exception you can also go with a boolean return:
private ?bool $valid = null;
private array $errors = [];
public function valid(): bool
{
// if the message is already validated we don't need to validate it again
if ($this->valid !== null) {
return $this->valid;
}
if (empty($this->data['title']) || !is_string($this->data['title'])) {
$this->errors[] = [
'field' => 'title',
'message' => 'Expected a valid title to be given.',
];
}
if (empty($this->data['locale'])
|| !is_string($this->data['locale']) ||
!in_array($this->data['locale'], ['en', 'de'])
) {
$this->errors[] = [
'field' => 'locale',
'message' => 'Expected locale "en" or "de" given.',
];
}
$this->valid = empty($this->errors);
return $this->valid;
}
public function getErrors(): array
{
if (null === $this->valid) {
throw new \LogicException('Run "valid" before calling "getErrors"');
}
return $this->errors;
}
All of the above methods I think are good ways for validations, another way would be using an own service which is validating the message. This specially make sense if the message can be extended with additional data which need to be validated.
$message = new CreateEventMessage($data);
$validator->validate($message);
So there are different ways of validating of the message, you also always need to ask how much you want to validate inside your application core. In most cases I would go with the first solution by validating it directly in the constructor. If your controller would need another type of validation to show better error messages you could provide some json schema standard. This type of standard allows you that you can not only validate it in a controller but also the client could validate it before triggering your api.
Where I personally prefer the pattern with to give an whole array into the message, it is very common that you design your message / command constructor with the data which is required:
$message = new CreateEventMessage($locale, $title);
Which advantage of disadvantage this have we will see in the Make Handlers extendable chapter.
The message handlers implements what will happen when the message is dispatch. In our case they are responsible for creating, modifying and removing an entity.
As mention already in the creating messages chapter
its common that there are also more specific messages like
ChangePasswordMessage
or PublishEventMessage
, which are modifying our
objects not in the typical crud way.
For our typical handlers which we created the handler need the Repository. The repository of our models is represented in our Handlers only be the RepositoryInterface, which make over the Dependency Inversion Principle possible that different implementation or changeable implementation could be behind it.
With the message we did already keep our handlers clean from request objects, sessions and other things which are only needed to receive or deliver something.
Also with the message object we already did make sure the data we are receiving are already validated. So we can here in our create and modify handler load the requested model and call the mappers to map directly the data to the model.
In the case of the Modify and Remove handler the identifier is represented
by an array (Map). The decision here was to go instead of using int $id
to
go with an array $identifier
to make it possible to modify or remove our
model not only by its id but also by any other unique identifier a model
could have. For example an uuid or an unique key field.
The handlers are in our case returning nothing or returning the object. They are not responsible how the object will be represented, this should not be part of them. The handlers can throw events which could trigger other processes like sending an email. Also other bounded contexts could listening to this events to update there data with the done changes.
The datamapper which we added to the CreateEventMessageHandler and ModifyEventMessageHandler make it possible that we don't need to duplicate the code to map data to our Model which are the same between created and modify.
But this is not the only advantage of it. If we are allowing multiple mappers we can make our create and modify handlers extendable. By introducing an interface for the mapping:
Also have here a look at the so called Visitor Pattern which make this type of things possible.
The requirement for making it extendable was that our messages allows that additional data can be provided to our handler. This is done over the introduced getData method.
Where it is good to keep your application core small of dependency it doesn't mean that all your vendor code is automatically infrastructure code.
The connection to infrastructure code is mostly done over an Interface using the Dependency Inversion Principle. PHP provides over PSR many common interfaces where no own interface need to be written.
At the time I'm written this chapter Matthias Noback has released an article about this topic which is worse to read: https://matthiasnoback.nl/2021/08/on-using-psr-abstractions/.
There are other common libraries which are for example abstractions like Flysystem library for local and remote filesystems access by Frank de Jonge. This type of libraries already provides interfaces you can use, mostly this interfaces are more bulletproof than if we would write our own. Also this kind of libraries can be inspiring how you can use the Adapter pattern to connect to your infrastructure code.
Also small libraries do not hurt to add to your application code, it should not have the need to reinvent the wheel, common libraries are example the webmozart/assert which provides you a good way to valid the data given to the message constructor. Or a small library like the ramsey/uuid to generate uuid4 strings for your application.
Things like how your code or library is integrated into a framework or how it is communicating with the database, orm should still definitely live in the infrastructure code.
A clean architecture, following the concepts of SOLID software engineering, specially single responsibility should make your code easier to test with the framework you want.
As there are Interfaces used to connect to your Infrastructure code, you will naturally create mocks for them. Where in somecases easier, you need to know about the inner structure of the class / method you want to test. This is something you want to avoid. The inner part of a method should be changable and aslong as you don't change input or output of that method the tests should not be needed to be changed.
In case of the RepositoryInterface we created in the Domain the solution is that we provide a InMemoryRepository. This InMemoryRepository can also be a small reference how our repository methods should behave and return things beside the interface definition.
Things like PSR 3 for logs already provides functional working implementation like the NullLogger which can also be used in the tests.
If testing without mocks is used you maybe need inside your tests use an instance of your infrastructure code. As an example using the symfony event dispatcher as a psr 14 implementation. So there is no need to create here your own implementation of an interface only for testing.
I can recommend to give testing without mocks a try, atleast I would avoid mocking things like models, message in your tests. Creating an instance of them and using them directly does make your tests a lot more maintainable and avoid that an inner refractoring of a service requires to rewrite all your tests. More about this topic you can find on Frank de Jonge blog in the https://blog.frankdejonge.nl/testing-without-mocking-frameworks/. article.
The AAA pattern is a way how to write tests, in most cases you are doing this kind of things naturally. But knowing that there exist this pattern help you to avoid violeting it.
AAA stands for Arrange, Act, Assert. Which are the 3 steps you should write your test.
First you arrange all you need to test the method in our case for a test with the handler, the handler itself with the required repository and the message we want to test:
// Arrange
$repository = new InMemoryEventRepository();
$handler = new CreateEventMessageHandler($repository);
$message = new CreateEventMessage(['locale' => 'en', 'title' => 'Test']);
After you arranged all you need you actually call what you want to test in the step of Act:
// Act
$event = $handler->__invoke($message);
After this we finish the test with asserting the expected result.
// Assert
$translation = $event->findTranslation('en');
$this->assertNotNull($translation);
$this->assertSame('en', $translation->getLocale());
$this->assertSame('Test', $translation->getTitle());
If doing this only once per test, the tests are very readable. In combination with mock-free testing this makes the application testing code readable and maintainable.
The infrastructure provides implementation for different Interfaces which are accessed in our application code. The interface should make the Infrastructure replaceable and our application core should work without it. In the following examples it is shown how to implement different ORMs and how to integrate our application code into different frameworks.
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon...
Coming soon ...
- Hexagonal Architecture
- Layers (clear structure)
- Dependency Inversion Principle (connection between layers)
- Adapter Pattern (connection to infrastructure)
- Visitor Pattern (extendability) ...
At first, I want to thank here all my colleagues from Sulu without the I would not be able to write this and all feedback they gave me. Specially here Thomas Schedler which always pushing us to look "beyond the tellerand" ;). I also want to thank here the spiral community which did help me a lot to integrate the Cycle ORM integration as an alternative persistence layer. A lot of things which I learned about Hexagonal architecture is coming from Matthias Noback blogs about this topic. So I also want to thank him here to share his knowledge with us, that we can create sustainable software. Another man which was also an inspiration for me is Frank do Jonge, creator of flysystem and enthusiast of testing without mocking, which did show me a way of making tests more maintainable.
And at the end I want to thank you, the reader of this, thank you for taking this time I hope you did find something useful in my "Project Rabbit Hole".
- https://web.archive.org/web/20170730135337/http://alistair.cockburn.us/Hexagonal+architecture
- https://martinfowler.com/tags/domain%20driven%20design.html
- https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)
- https://en.wikipedia.org/wiki/Visitor_pattern
- https://en.wikipedia.org/wiki/SOLID
- https://www.php-fig.org/psr/
- https://en.wikipedia.org/wiki/Single-responsibility_principle
- https://flysystem.thephpleague.com/v2/docs/architecture/
- https://blog.frankdejonge.nl/testing-without-mocking-frameworks/
- https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80
- https://matthiasnoback.nl/tags/hexagonal%20architecture/
- https://matthiasnoback.nl/2021/08/on-using-psr-abstractions/
- https://matthiasnoback.nl/2020/02/is-all-code-in-vendor-infrastructure-code/