loading...

Hexagonal Architecture doesn't really work

grahamcox82 profile image Graham Cox ・2 min read

For various reasons, I've been reading a lot of late on Hexagonal Architecture / Ports and Adapters. On the face of it, this seems really neat.

Basically, you define an inner domain, with the domain models, interfaces for all of the ports that the domain depends on - the driven ports, and interfaces for the ports that the domain exposes - the driving ports. The domain then also has a service that implements the driving ports on terms of the driven ports.

The benefit here is that anything depending on the Driving ports needs know nothing about the internals, and anything implementing the Driven ports can be plugged in. This means, for example, we can plug the domain into a web server, a command line app or an Android app with no business logic changes. It also means we can plug in a Postgres database, or a DynamoDB database with no changes to the business logic.

This all sounds really good. But does it work?

My very first thought was about changing database technology, and the fact that different databases do different things. And specifically about text searching.

If I decide to use Postgres then it has full text searching built in, so I only need one port and one adapter. If I decide to use DynamoDB then it does not have support for text searching, so I would need a second port for a full text engine, and an adapter for CloudSearch.

The problem here is that the choice of infrastructure defines the ports, and this is exactly Not the point.

The alternative is that we implement the DataStore and the TextSearch ports for Postgres, but have the TextSearch just do nothing when indexing documents. Except this is a waste of effort. Worse, it seems reasonable that the TextSearch port returns the ID of the matching records for the DataStore port to fetch, but if they're both the same database then this is a performance sink - it's two queries when one would suffice.

So, unless someone can enlighten me (please!), it seems that this architecture is potentially very tied to the infrastructure decisions after all.

Discussion

markdown guide
 

Does it work if you think about the database as the database solution rather than the database itself? In one solution the hexagon at that level contains Postgres. In the other it contains DynamoDB and CloudSearch. The ports exposed at the boundary of the hexagon would then be the same. I'm still getting my head round this topic so feel free to come back at me if this is a gross simplification of what you are highlighting.

 

I wondered about that after I wrote the post, but that also falls down if I want to then replace CloudSearch with Elastisearch but not replace DynamoDB. Then I'm reworking a part of one adapter and not just swapping it out for a different one.

 

Yes, I see what you mean...

You could see your 'big' hexagon as having within it two smaller hexagons that represent how that particular solution is composed.

One for cloud search and one for DynamoDB. Which I think is okay if neither DynamoDB or CloudSearch are used independently anywhere else. The ports between CloudSearch and DynamoDB are on the internal faces as they don't affect the larger solution. Things that are used outside are on the external faces.

Then you could still swap out one of those smaller hexagons for one that supported the same combination of ports.

I don't know myself if that deviates too far from the spirit of hexagonal architecture though.

Hmm - That's interesting.

So you end up with:

  • The actual service in question, which exposes an XxxRepository port.
  • An implementation of the XxxRepository that just calls Postgres
  • An implementation of the XxxRepository that is a hexagon of its own, and which exposes two ports - XxxDataStoreRepository and XxxSearchRepository
  • An implementation of XxxDataStoreRepository for DynamoDB
  • An implementation of XxxSearchRepository for CloudSearch
  • An implementation of XxxSearchRepository for Elasticsearch

It works. It feels quite complicated, but it works. :)

 

Graham, I agree that some parts of infrastructure define the ports. Anyway, it is not the infrastructure itself, but your requirements. You can always abstract everything, as Mark pointed out you can use a composed adapter. But on the other hand, performance would be negatively affected or you would need extra ceremony (and code) to do what you want.
I can mention another point on the same example of changing between Postgres and Mongo. Postgres will have integer autogenerated IDs, and Mongo will only use UUIDs. This is a problem when you define repositories in a typed language (like Java) and use something as Repository (as suggested in Spring or commonly implemented in DDD examples). You can always use another field as a real primary key in the database but this would impact the performance. So finally you need to choose what is going to be abstracted. That's the developer works, to choose between trade-offs.