Sunday, 29 March 2020

Hexagonal architecture: don't get lost on your right-side

Fan of hexagonal architecture and its promises for a very long time, I often spent time to understand it, explain it but also to demystify its implementation. On the other hand, I've often limited my explanations to the simple cases. What we call the “happy path” where everything goes well. In this first article of an upcoming miniseries, I want to talk about some implementation details as well as traps in which it is very easy to fall. Finally, I will take some time to talk about how to test your hexagon, suggesting a non-orthodox strategy. The idea being to save you a little time during your implementations of this incredibly useful architectural pattern.

The "non-cohesive right-side adapter" anti-pattern

We will be starting today with a design-smell: the non-cohesive right-side adapter. If you do not know hexagonal architecture yet (a.k.a.: ports and adapters), I suggest you start by reading my previous post on the subject which summarizes all this. Because to explain the non-cohesive right adapter anti-pattern, we are just going to recall here a few fundamentals about Alistair COCKBURN’s hexagonal architecture:
  • We split our software in 2 distinct regions: 
    • The inside (for the Domain / business code that we call "the Hexagon")
    • The outside (for the Infrastructure / tech code)
  • To enter the Hexagon and interact with the domain, we talk to a port that we call "driver port" or "left-side port"
  • The domain code will then use one or more third-party systems (data stores, web APIs, etc) to gather information or trigger side-effects. To do so, the business code must remain at its business-level semantic. It is done by using one or more "driven ports" or "right-side ports".
  • A port is therefore an interface belonging to the Domain using business semantics to express either requests that we address to our system (left-side port) or external interactions that it performs along the way to achieve its goal (right-side port). I like to see ports as drawbridges we are using to come and go between the infrastructure side and the Domain one (analogy that Cyrille MARTRAIRE had whispered to me a very long time ago)
  • In terms of dependency, the infrastructure code knows and references the Domain one. But let's be more precise here (BTW, thank you for your feedback Alistair ;-): only the left-side infrastructure code knows the Domain actually (to interact with it through a left-side port).
  • On the other hand, the Domain (business) code must never reference any technical/infrastructure-level code. It works at runtime thanks to the Dependency Inversion principle, that Gerard MESZAROS called once "configurable dependencies" (which lends itself very well to Alistair's pattern)
  • An adapter is a piece of code which makes it possible to pass from one world to another (infra => domain or domain => infra) and which takes care in particular of converting the data structures of a world ( ex: JSON or DTO) to the other (ex: POJO or POCO of the domain)
  • There are 2 kind of adapters:
    • "Left-side adapters" which use a left-side port to interact with the Domain (thus having an aggregation-type relationship with it)
    • "Right-side adapters" which implement a right-side port instance

The problem with the right wing...

You may have noticed here that I did not say that a right-side adapter implements multiple right-side ports. It was intentional.
Because if you did, there is a good chance that you will land on the anti-pattern that I want to mention here: the non-cohesive right adapter.
This is what happens when you give too much responsibility to a right-side adapter: you risk then deporting part of the Domain logic of your hexagon into one of its peripheral elements on the infrastructure side (i.e. the adapter). This is what happens especially when an adapter references and uses another adapter directly.
Of course, this is something I strongly advise against doing. Indeed, you need to keep all the business orchestration logic at the Domain level and not to distribute it here and there at the infrastructure code level. But since I've seen that everyone at least falls into this trap once, it's worth mentioning. Avoid this situation if you don't want to end up with an anemic Domain.
(click on the image to zoom)
 (click on the image to zoom)

Worst. In that case, those who test only the center of the hexagon (i.e. the domain code) by stubbing the adapters all around would miss out on lots of code and potential bugs. Those would be tucked away in your untested adapters.
It is indeed extremely easy to deceive yourself by putting a lot of implicit in the behaviours of your adapter stubs. You will very often put in those stubs what you think the adapters will do or should do. But the risk afterwards is to fail to implement the concrete adapter properly (when you will forget all the implicit you initially put in your stubs in the first place). This will give you a false confidence with a fully green test harness, but potentially buggy behaviours in production (when using the real concrete right-side adapters).

Let's talk a little bit about test strategy

I have already written and spoken on the issue but since I have not yet translated into English my most specific article on the subject, I will just clarify here some essential terms to understand the rest of the discussion.
After more than 15 years of practicing TDD at work, I have arrived over the years to a thrifty form of outside-in TDD which suits 90% of my projects and contexts very well. Outside-in means that I consider my system (often a web API) as a whole and from the outside. A bit like a big black box that I’m going to grow and improve gradually by writing acceptance tests that will drive its behaviours and reliefs (see the fantastic GOOS book from Nat Pryce and Steve Freeman if you want to know more). Those acceptance tests aren’t interested in implementation details (i.e. how the inside of the black-box is coded) but just on how to interact with the box in order to have the expected reactions and behaviours. This lets me easily change the internal implementation of my black box whenever I want without slowing me down or breaking fragile tests.
My acceptance tests are therefore coarse grained tests covering the full box (here, the entire hexagon) but not using real technical protocols neither real data stores in order to be the fastest possible (I'm addicted to short feedbacks and thus use a tool to run my tests every time I change a line of code).
Besides these acceptance tests which constitute 80% of my strategy, I generally write some fine-grained unit tests (the small tests of the "double loop" when I need them), some integration tests, and some contract tests for my stubs to check that they behaves like the real external dependencies they substitute with (and also to detect when something change externally). The rest of this article will mainly talk about my coarse-grained acceptance tests.

The best possible trade-off

This parenthesis about testing terminology and my default test strategy being closed, let's focus on our initial problem: how to avoid writing incomplete tests allowing bugs to happen at runtime when we deploy our system on our server.
My recommendation: write acceptance tests that cover your entire hexagon, except for the I/O at the border. Yes, you heard me right: I advise you to write acceptance tests that use all your concrete adapters and not stubs for them. The only thing you will have to stub are network calls, disk or database access which are made by your adapters. By doing so, you test all your code in full assembly mode (without illusions or unpleasant surprises in production therefore), but blazing fast ;-)

 (click on the image to zoom)
(click on the image to zoom)

 

Can you give me an Example?

Sure. Let's take an Example to make the implicit, explicit.
Imagine that you have to code a sitemap.xml file generator for the SEO of a web platform that has a completely dynamic content controlled by several business lines (e-biz, marketing). This hexagonal micro-service could be a web API called daily in order to regenerate all sitemaps of the related website. You call a HTTP POST request, and all the sitemap files are then updated and published. (as a side-note: Please, don't tell my mum that I mentioned microservices in one of my articles ;-)
Okay. Well instead of writing acceptance tests like this where every adapter is stubbed:
  (click on the image to zoom the code)

I rather advise you to write acceptance tests in which you will include your concrete adapters and only stub their last-mile I/O. It is worth mentioning here that I’m using my ASP.NET web controller as my left-side adapter. Anyway, it looks like this: 
(click on the image to zoom the code)

There is a better way

Of course, in order to have readable, concise (no more than 10/15 lines long) and expressive acceptance tests (i.e. by seeing clearly the values ​​transmitted to your stubs), you will generally go through builders for your stubs, mocks. You can also use them to build your hexagon (which is a 3 steps initialization process). I didn't show it earlier because it would have been less clear for my example but a target version might look like this:
 (click on the image to zoom the code)

A good adapter is a pretty dumb adapter

Be careful. Testing your full hexagon with its concrete adapters should not prevent you from putting all your business logic or orchestration at the heart of your Domain code (i.e. the heart of the hexagon).
A good adapter is a pretty dumb adapter. As a reminder, its role is to translate a model linked to infrastructure to a business model and vice versa + to make the link with the necessary technology. Above all, you should not have a situation where an adapter uses another adapter to play tricks or optimize certain calls.
When you are doing Domain Driven Design, you aim to reduce as much as possible the accidental complexity of your system (i.e. the complexity related to your tools, idioms or programming language) while trying to focus only on essential complexity (i.e. the complexity related to the business problem to be solved). In that context, the infrastructure code of your hexagon should be a pass-through. No more, no less (note: we won’t talk about Anti-corruption Layers here. That will be worth another article).

Expected objections

People generally agree on the need to keep the domain in the center of the Hexagon. On the other hand, I've got some objections related to my testing strategy. Let's review some of them.


First, it could be objected here that my acceptance tests are a bit hybrid and that they cover both Domain considerations but also technical ones. Not very orthodox...
Well... actually I don't realy care about orthodoxy or by-the-book-ism ;-) As soon as you can explain your choices and the trade-offs you, I'm ok with that. Moreover it can be completely transparent if you make sure that each of your acceptance tests covers business behaviours of your system (service / API / microservice...). It's up to you to work on the expressiveness of your tests: their names, their simplicity, their ubiquitous language, but also the conciseness of the "arrange" steps (using DSL-like expressivity for example).
I've landed on this new strategy this year after tons of trials and errors. Before that I was implementing the very same test strategy often put forward by the London friends from Cucumber (Seb Rose, Steve Tooke and Aslak Hellesøy). That one aims to combine:
  • A lot of acceptance tests but for which we stub all the adapters (thus blazing fast tests)
  • And some additional integration/contract tests aside, in another project. These contract tests verify that our adapter stubs used in our acceptance tests have exactly the same behaviour as their real concrete adapters (the one that we package and deliver with our hexagon in production). These last integration tests are therefore much slower and run more rarely (mainly on the dev factory, not within my local NCrunch automatic runner). But no surprise here: cause it's an expected trade-off

So, we have had a really interesting situation here, which allowed us to have tons of super-fast acceptance tests AND to be confident enough about our right-side adapter stubs to be true to reality enough (for some reference scenarios).
I talk about it in the past-tense here because I haven't really managed to be confident enough with this setup during my various experiences with different teams.

Blind spot

Indeed, I regularly had problems with this strategy because we didn't cover enough cases or errors or exceptions within those contract tests of our right-side adapters. Too many happy paths (basic scenarios where everything goes well), not enough corner cases and exceptions in those contract tests. To put it another way, our acceptance tests were asking our stubs on many more cases than what was planned in their contract tests.
We developers are attracted to the happy path as much as moths are to a lit bulb. It seems to be one of our determinism (quite the opposite of QA people ;-) I knew that already. But I observed these situations carefully to try to understand what had made us fail here (in my various teams and contexts). I came to the conclusion that it was because these integration tests - in addition to being very "plumbing oriented" (more legacy-oriented than domain-oriented) - were much slower to run than our unit or acceptance ones. Every time you add a new parameter combination, it increases the overall test harness execution latency. That’s why my people paid less attention to it. A little bit in mode: "anyway, it's an integration test for a stub... it should do the job but we're not going to spend too much time and effort on it either".
As a consequence, we used to test a lot less case combinations within these contract tests for our stubs than what we used to do in our other tests (i.e. coarse-grained acceptance tests or fine-grained unit tests).

Stubbing less

The side-effect of this was that combination of contract tests and acceptance tests was not sufficient, to manage to catch all the bugs or plumbing problems in our final assembly. It is therefore for these reasons that I finally arrived at the test strategy that I presented to you in this article, and which includes the concrete adapters in our acceptance tests. On the other hand, I continue to use this strategy of testing contracts for external components or third-party APIs. But now I stub less things, my stubs only cover a very fine part of my system.

One can Pick hexagonal architecture for different reasons

This one is important. As a final warning, I should also point out that my testing strategy has been designed and worked well in my contexts so far. Of course, I’m not saying here that this is a one size fits all situation (I don’t even think that such a situation exists).
In particular, my contexts aren’t the one faced by Alistair when he created the pattern. Back in the days, Alistair had to find a solution in order to survive with a huge number of connectivity and technologies, to avoid his Domain logic to suffer from a combinatorial explosion (for a weather forecast system).
At work, I mainly use hexagonal architecture pattern because it allows me to split my business code from my technical one. Not to combine or easily switch my left-side technologies and adapters. Reason why I usually have a unique exposure for my business services which fits exactly my purpose and context: REST-like HTTP for a web API, Graph QL sometimes for website backends, Aeron middleware for a low latency service, AMQP-based middleware for some HA, resilient and scalable services, RPC for... (no, I'm kidding, I hate so much the RPC paradigm ;-)
More than that, it is very likely that the communication middleware I picked has more impacts on my business code interactions (left-side port) than a simple switch of Adapters. In some cases, the middleware technology I use may even impact my programming paradigm (event-driven reactive programming/Classical OOP/FP/lame OOP/transaction script). Reason why my left-side port may often become a leaky abstraction.
As a consequence, I only need one adapter on the left side. This saves me from having to run all my acceptance tests as many times as I would have different left-side exposure technologies. That is worth mentioning (at least for people that would have a similar objective that Alistair had).
And when my goal is to build a REST-like web APIs, I even use my web controllers as left-side adapters instead of creating another ad-hoc adapter type (this is something we are legion to do, if I recall Everyone who chatted with me during conferences or my DDD training sessions).

An interesting side-effect

The technique I was promoting here to test everything but not the I/O will put you in a very comfortable situation at the end of the day. Indeed:
  • your acceptance tests will cover a base code very faithful to the reality of the production (my main driver here)
  • your acceptance test harness will allow you to calmly refactor your code base in the event that you have made a mistake and put some business behaviour into your adapters. Thanks to this test harness covering everything, the move of the wrongly located business code from the right-side Adapter to the Domain can be done without any risk.

Perhaps we could find an easy way (other than pairing or code review) to prevent less experienced people from falling into this trap. For the moment, this ability to easily refactor -a posteriori- when we screwed this (or when we took a little short-term technical debt), was more than enough for my projects.

Conclusion

Since this post is long enough, I’ll be brief:
  1. Always go through the center of the hexagon, do not connect your right-side adapters to each other
  2. Do not code YOUR domain logic or YOUR domain orchestration logic in your adapters
  3. Test your entire hexagon (including adapters). To do so, only stub your last miles I/O from your right-side adapters.
  4. Aside, keep testing your test doubles or stubs of external dependencies against real implementations via contract-based integration tests.

The next 2 articles in this series dedicated to hexagonal architecture will talk about the subject of health checks (how to know if our hexagon is in shape or not) but also about the comparison with an alternative to the pattern: Functional Core (with imperative shell). 
Happy Coding! See you soon.
Thomas

No comments:

Post a comment