A few days ago, I posted an article to warn people about some pitfalls one should avoid when implementing an hexagonal architecture. One of these pitfalls is to leak part of our domain logic to one or more adapters (therefore on the infrastructure code side). Whereas I'm convinced that this is something to be avoided at all cost (in order for our Domain code to remain coherent and not entangled with technical issues), I would like here to linger a little and revalue an area that I have myself got into the habit of shouting slightly: the code outside the hexagon. Today, the passionate practitioner of Domain Driven Design that I am wants to assert that the right-side adapters of our hexagonal architecture deserve much better than the treatment usually reserved for them. What if this infrastructure code could ultimately be just as important? What if it could be the place of major challenges for our systems?
The origin of DDD
DDD started from an attempt by Eric Evans to give more autonomy to the people and different teams
who worked - sometimes with too much friction - in the same structure where he was also (finance industry).
To get there, Eric had the idea of bringing out his key notion of Bounded Contexts. A Bounded Context is a linguistic and conceptual boundary where words, people and models that apply are consistent and used to solve specific Domain problems. Specific problems of business people who speak the same language and share the same concerns. We can therefore say that DDD militates for use-case-centric specific models, modelled on groups of people.
Of course, all this being very specific to an organization and the business processes set up in it. In the same company, we can find, for example, one or more Bounded Contexts related to "marketing", another to "accounting", another one specific to "delivery service", etc. When doing DDD, we generally try to identify and characterize these different Contexts to know where we are, whether it really brings business value or only relates to support functions. It will drive our effort and ways of working out.
In the process, we are also trying to identify the other Contexts with which we will have to collaborate. All this is usually illustrated in a diagram which represents Bounded Contexts (represented like big potatoes) and which one names Context Map (see the example below).
The entire strategic chapters of the DDD describes these relationships between Contexts (human and technical) and provides us with patterns to manage this or that situation. To connect and communicate between Context given the power relations involved.
Here is a naïve example of Context Map applied to a hotel group which here distributes its hotel rooms through its own distribution platform (web and apps).
Context Map from the hospitality industry
SOA FTW!
When practicing DDD, the size of a service does not matter, we are rather looking for its proper alignment with the business (and therefore within a Bounded Context).
This is why DDD may help a lot people to escape the micro-services quagmire (where people are only focusing on the size...)
Within the same Bounded Context, we will often find one or more services (web APIs in general nowadays). Since Alistair's pattern is very handy for implementing a service I will often use it to implement them.
The general case is to have one hexagon per process (which can be scaled out with multiple containers or VMs) but it is not mandatory.
In some cases, one can assembly multiple hexagons within the same process (In-proc). Every hexagon will interact with others using the same ports but with different Adapters in between that will only do in-proc calls (instead of network calls). We usually call this multiple-hexagon situation as Modular Monolith, which is a good thing actually (as opposed to old-school Monoliths or even worst: distributed Monoliths/Distributed big-ball-of-mud ;-)
One can have multiple hexagons or services in a Bounded Context.
Or even multiple hexagons in the same process (Modular Monolith)
A common DNA
Making autonomous models, splitting them by Context and preventing them from being entangled. Doesn't ring a bell to you?
No surprise Alistair’s pattern suits really well DDD practitioners. The ports and adapters effectively allow us to prevent DTOs of an external API (often coming from another context therefore) from parasitizing and entering our hexagon which is supposed to only manage problems from the Bounded Context to which it belongs.
Without that, coupling between those 2 different Contexts would be hellish because we would have some bits other contexts within our business code. Imagine now the impact on our code in case the other Context dev team constantly changes their API or DTO contracts without asking our opinion. To protect ourselves from such a situation, the DDD toolbox offers us an interesting pattern: The Anti-Corruption Layer (ACL).
Anti-Corruption what?!?
It’s like a shock absorber between 2 Contexts. We use it to reduce the coupling with another Context. By doing so, we isolate our coupling in a small and well protected box (with limited access): the ACL.
Then, any untimely change of the external context will have very little impact on our business code which acts as a consumer of the ACL and no longer directly of the unstable code coming from the external context.
No ACL here, so the model of the other Bounded Context leaks everywhere within our hexagon
Here, our ACL Adapter prevents models of another Bounded Context from entering our hexagon
- external gateway/service/intermediate API
- dedicated in-middle database (for old systems)
- or just an in-proc adapter within a hexagonal architecture.
Which Eurythmics to choose for your ACL?
Any good writing on DDD must now have a "Heuristics" moment ;-) Here is mine:
If you have several consumers originated from your Bounded Context (potentially several APIs or components) who need to interact like you with the same portion of the other Bounded Context outside, it is in your best interest to do emerge a shared facade service (API) that would act as an ACL.
If you only have one consumer for these interactions with another Bounded Contexts (i.e. your hexagonal domain code), you can just implement your needed ACL at the Adapter level of your Hexagonal Architecture.
It is very common to start by implementing an in-proc adapter here, and then to move out as a stand-alone API if the need arises.
If you have several consumers originated from your Bounded Context (potentially several APIs or components) who need to interact like you with the same portion of the other Bounded Context outside, it is in your best interest to do emerge a shared facade service (API) that would act as an ACL.
If you only have one consumer for these interactions with another Bounded Contexts (i.e. your hexagonal domain code), you can just implement your needed ACL at the Adapter level of your Hexagonal Architecture.
It is very common to start by implementing an in-proc adapter here, and then to move out as a stand-alone API if the need arises.
An Anti-Corruption Layer can be hosted in an external dedicated process/API/gateway
It is quite logical because none of those existing external Bounded Context systems was made specifically for you and your needs. They usually speak with their own language and do not directly answer questions you ask yourself in your own other Bounded Context. In most cases, the external systems you will need to operate with are rotten legacy systems, or a mix between an old system and a brand new API that only works on a limited number of cases.
The ACL-Adapter of your hexagonal architecture will therefore very often have to call these multiple sources to be able to offer a consolidated vision to your domain.
For instance:
- Calling a first service of another BC to recover incomplete data
- Translating or adapting part of its response into another data structure ready to be used to forge an intelligible request to another external service (often belonging to the same second BC but not always)
- Sending this second request to the second system or endpoint
- Getting the result of this second call and adapt the whole set to produce a response that will finally suit your own Hexagon with its Domain-level semantic
Example of ACL-Adapter orchestrating different calls to different back-ends / APIs of another BC
My Domain is not your domain
In other cases, you will find important that the center of your hexagon explicitly goes to look for 2 different things/concepts via two different operations or two different right-side ports before doing some stuffs or even assembling them in order to become something useful for you.
There is no silver bullet
Do not expect me to give you a universal or absolute rule to answer this question. It's Design. And of course, it will depend on your context and your Domain.
The right question to be asked every time is: is it legitimate and useful for my domain code to be aware of these concepts or details?
If so, your hexagonal Domain code should explain these interactions and exchanges. If not (when we don't want to be polluted by the messy external BC for instance), this knowledge should rather remain at your right-side adapter level.
Mea culpa
In the light of these cases and explanations, you will understand why I really regret having written in my previous post that a "good adapter is a pretty stupid adapter". This is the case when there is a simple 1-to-1 relationship between your hexagon and another external system.But in practice it is quite often the opposite (and even more so if your adapter is an ACL).
And therein lies the paradox. For years I have only been focusing on the Hexagonal Domain code. With practice and all the non-trivial cases encountered in my career, I realized that
These Adapters are much more than just Extras.
As such, they also deserve much better than just being stubbed in our hexagonal architecture acceptance tests
But on that testing strategy topic, I will come back very shortly with a new episode of this series devoted to hexagonal architecture.
Stay tuned!