Recently, I realized my first mission on a Hexagonal Architecture project with a special implementation: Domain Driven Hexagon.
This approach was created by Sairyss in a Github repository (link at the bottom of the article) in 2020. It is increasingly popular, but it tends to have some trouble democratizing. Actually, it’s quite understandable because it is hard to apprehend initially (and even several months after). However, it is really powerful, and it deserves more visibility.
To give you more context, Domain Driven Hexagon is based on Hexagonal Architecture theorized by Alistair CockBurn in 2005. It deals with Domain Driven Design (DDD), introduced by Eric Evans in 2004. DDD aims to help all stakeholders to create a shared vision and lexicon in order to better apprehend its complexity. Domain Driven Hexagon also uses some concepts of Command Query Responsibility Segregation (CQRS) pattern. Which was coined by Bertrand Meyer in 1988.
Spoiler alert: this article is not supposed to give the pros and cons of DDH. It aims to be helpful for a developer who begins on a project with such an architecture. I just want to give you some small keys that I wish I had when I dove into the code.
If you get to those lines, this article is surely made for you. So let’s dive in. Here are 9 points that I hope will demystify a little some key concepts of DDH:
- A back-end divided into modules
- Inverse dependency injection with ports and adapter
- Separate commands and queries
- The importance of entities
- The distinction between entities and aggregates
- Never throw domain errors
- Add event management
- Your code deserves more DTOs (Data Transfer Objects)
- Make the difference between controller, presenter, and resolver
Info: To improve readability, this article does not contain code. If you want to see implementation examples, I created a repository (link at the end of the article). It represents some parts of a pizza restaurant with a DDH approach. Besides, all examples of the article are linked with this pizza restaurant.
A back-end divided into three parts
In DDD (Domain Driven Design), one of the first objectives is to determine bounded contexts. A bounded context is a set of things from the business that can have a unified model together. And if you add one other thing to this model, you risk having an ambiguity on a word in the vocabulary.
For example, in our pizza restaurant, something as simple as a dish has different meanings: for a customer, it is something on the menu, a receipt for the cook, and a command for the server. We may define 3 bounded contexts, one for each business in a restaurant (service, cooking, and client).
In DDH, each bounded context will be implemented as a module. In a module there are always three parts which can be represented as following:
- The domain is the center part. It contains all the business logic.
- On the left, closest to the clients (human or other services) is the interface. It contains all the elements that allow the back-end to be reached by clients.
- Finally, on the right, closest to external services on which the app depends, we find the infrastructure. It is the back-end part that allows the domain to be indifferent to selected technical solutions. It contains the implementation of the connection to those services.
Diagram inspired by Sayriss repo. (NB: I will be consistent in the colors of every schema in this article. It means, for example, that I will use purple only for elements that should be in the domain)
Inverse dependency injection with ports and adapters
As DDH aims to respect SOLID principles, this concept is respected. The domain should be independent (from everything). It is the hexagonal architecture that will help us achieve that.
Dependency inversion principle, one of the SOLID principle
To do it, the domain will tell with interfaces what it expects to have out of its border. Those interfaces are the ports. On the other side of the border, in the infrastructure, adapters are the classes that implement the ports. They contain the particular logic linked to the chosen external service.
A simple metaphor in our pizza restaurant is the hiring of a new delivery guy. The only question that the owner can ask the candidate is if he is able to go from the pizzeria to an address with the pizzas. If the candidate can, then the owner can hire him. It does not matter that this delivery guy goes with a bike or with a motorcycle, if he writes the address on his phone or on a GPS...
Example of a port and two potentials adapters for pizza command delivery
Warning: you must declare which port is linked to which adapter in the module files. Otherwise, you may face errors about dependencies.
Separate commands and queries
In the domain, you will have to separate methods used to get data from methods that allow you to change data. It is a good way to avoid side effects with retrieval and changes of data at the same moment. Commands are the methods that modify data, while queries are those used to get data.
The distinction between command and query segregation and common implementation
In the code, to execute commands and queries you may use a bus. It avoids coupling with a service that contains the implementation. In this case, a command or a query is always a combination of two files. One with the signature and the return type. It is the command or query file. The other contains the logic executed when the method is called via the bus. It is the command handler or query handler file.
A command and its command handler files in the domain
I had some misconceptions about how to use commands and queries, here are some big learnings I got:
- As seen before, the domain must be independent. So in the handler, only call ports methods. Try to avoid direct calls to a repository or to an external service;
- Handlers can not directly call other commands or queries. However, there are ways to indirectly call them:
- If you need to call commands/queries from the same module, you must create a service in the module. In this service, you can create a method that calls the necessary commands/queries and maybe add some business logic;
- If you need to call commands/queries from another module, you must call the presenter of this module (cf point 8);
- The command/query handler can create events (cf point 7).
The importance of entities
Here is an important and not-so-simple aspect of DDH. In common software architectures, a reflex is to create objects corresponding to database elements. In addition, business logic linked to those objects is split into several files.
On the contrary, in DDH, the business logic dictates the objects that should be in the domain. They are the entities. They need to reflect a business reality, not a data persisting model. Furthermore, the aim is to put in the entity all the business logic linked to this entity. This helps us prevent the Anemic Domain Model antipattern.
It is those entities that you will have to manipulate inside the domain to perform actions. Each one must have a unique identifier. To populate them from the persistence system you should use mappers.
Another important role of entities is to protect the domain invariants. Those invariants are business rules considered true everywhere in the domain. (As an example, in our pizza restaurant, the price of a pizza is always strictly positive). Entities must have a constructor that allows the construction of the object only if invariants are respected. During an entity's life cycle, internal methods do not change its state if it goes against the invariants.
A business case and its implementation
The distinction between entities and aggregates
Now that you’re aware of entities’ power, let’s increase complexity with aggregates.
Aggregates are a gathering of domain objects (entities or value objects) that make sense only together. They offer a second container for business logic inside the domain.
As an example, you may think about a pizza command in a restaurant. The command is always linked to a client and contains one or several pizzas. It also always has a price. Clients and pizzas could be entities, while the price could be a value object in our domain. Actually, those three things are useless alone. What is the interest of the client if he does not consume pizzas? Why cook pizzas if they are not related to a command?
I had some trouble coding my first aggregates, here are some of my learnings:
- Aggregates, as entities, are responsible for the domain invariants. They must forbid modifications of their attributes that go against business rules. For example, our pizza command aggregate must not accept empty content. Hence, by definition, a command exists only if it contains at least one pizza;
- Aggregates must forbid independent modification of one of its attributes. Modifications must be done only through aggregates’ methods. Thanks to that, each one of the attributes is changed coherently with the others;
- Sometimes, aggregates need a parameter from another module (for example, you need a client from the client module in the pizza command). To avoid dependency, you can use the entity’s id rather than the entity itself as an attribute of your aggregate.
Aggregate does not modify data separately
Never throw domain errors
As you just understand, we must never go against domain invariants. However, attempts to transgress those rules can happen.
Something comforting is that those attempts are predictable because you know your business rules. So you can imagine the way clients may try to bypass them. It is therefore possible to create domain errors that are custom error classes with specific error codes. Each code is associated with a potential error linked to a business rule.
With this approach, three main advantages appear:
- Exceptions are only used for unpredictable cases that need to be investigated. Let's have a metaphor in our pizza restaurant. If you order a pizza that is not on the menu. You expect the waiter to tell you “sorry it is not possible” (our domain error) not to turn red and to scream(the exception);
- An error can be returned to the user as if it was “like” valid data. You may want to take a look at the Result Class of my repo, inspired by this article, for that purpose;
- A logic for each error case can be implemented. It avoids using a simple try/catch with a unique treatment for every error encountered. Thanks to that, the code is more robust.
As said before, exceptions should be kept only for unpredictable errors. And that is only those exceptions that we want in our error logs to be able to react quickly. Domain errors can be in the logs, but as info or warnings (to keep a track of them). The sooner you implement this, the faster you get useful and clear logs 😏
Error management in the domain
Add event management
Now that we spoke about errors, let’s think about when everything goes well. You may want to trigger methods in several parts of the domain, even in an external microservice, when some changes are made. How are you supposed to deal with it?
A first solution would be to extract the logic of calls into a specific service. But with this approach, a coupling is created inside the domain, and it is the opposite of DDH goal. A better solution is to build a communication system between our modules or with the outside. It is where event management is interesting.
Difference between the two solutions
DDH architecture distinguishes two event types :
- Domain events: They are events listened only inside the domain. They are usually managed by an internal event dispatcher;
- Integration events: Those events are for the outside world of our domain. They are used to trigger external services. Typically, you can use queue services like Kafka or RabbitMQ to deal with them.
In addition to avoiding coupling, events can be used to keep track of executed actions (if their calls are saved in the database for example). It gives a better understanding of state changes in the application.
Your code deserves more DTO (Data Transfer Objects)
At this step, you may be clear with the fact that the domain must not be dependent on something. Therefore, it should not depend on the format of the data sent by the client. Nor the format of data that the client expects to receive.
Here is the aim of Data Transfer Objects (DTOs). They contain sent or received data in a coherent format (with the domain or client needs). Only data formatting logic can be found in those objects, but the format of the data must be insured at this step to ensure that the model will be able to use this data.
On one side, Request DTOs will be instantiated inside the entry point of the domain with client data. Then, only them will be used to populate entities and aggregates. On the other side, Response DTOs will be used to map domain data into the format expected by the client.
Use of DTOs at an entry point
A good practice I learnt concerns the response DTOs: always whitelist attributes. It means always enumerate the properties you expect to have in your DTO. Avoid logic that returns all properties of the object except x. With that, even if you change your domain data model and add properties, you will not have data leaks due to a forgotten update of the dto.
Make difference between controller, presenter, and resolver
In order to use all the mechanisms seen previously, we need entry points for the domain. It is the controller/presenter/resolver role. Those elements are in the interface. They are used to receive the requests from outside the module and to execute the appropriate logic inside the domain.
In my last project, we used 3 different entry points:
- Resolver, which is used to get queries from GraphQL;
- Controller, which gets REST queries, for example if our back-end is going to have an API role;
- Presenter, which is an internal entry gate between modules. It is useful because commands/queries of a module must only be called inside this module. If module A needs to call a command/query of module B, you should call a route in the presenter of module B.
How a query from module B is called from module A with presenter
In an entry point, logic is always the same:
- Format the received data to correspond to the needs of the domain. For that purpose, mappers and request DTOs are our friends;
- Trigger the logic needed with commands and queries of the module;
- Format the response to correspond to the need of the client (here again, thanks to mappers and response DTOs);
- Return the data to the user.
Pro tips: in order to avoid duplication you may prefer to put the data validation inside the command or query handler rather than in the entry point. Otherwise, if you’ve several types of entry points for the same feature, the logic will be in them all.
There is no real theory about how many entry points to have. The only thing is that it is recommended to have one entry point per use case. For our example with the pizza restaurant, we would have entry points for orders, entry points to ask for the spicy sauce, entry points which deal with new clients,..
Here we are, after this walk in the park together. You may have expected other information into those 9 points, but I had to make choices. Nevertheless, I hope that this article will allow you to face your project with more knowledge than 10 minutes ago. To sum up all those concepts, here is the journey of a request to get client information from its id
How a request for client information is processed
A last point to remember: DDH architecture is really complex. Facing a lot of uncertainties at the beginning (even months after) is normal. So hang in there, be curious and soon DDH will have a snowball's chance in hell against you.
Github repository with implementation examples: