Categories: Architecture, Programming
Introduction
A friend and colleague of mine was recently praising the Hexagonal Architecture design pattern (also known as Ports and Adapters). I hadn’t heard of it so did a little research.
To my surprise it was actually proposed in 2005 and is well-known in at least some circles. It’s not terribly complicated, and it’s not particularly radical. However I think it’s a nice idea and an improvement over the traditional “layered architecture”.
The Problem
Hexagonal architecture is a technical code pattern which can be useful to structure non-trivial codebases. As I see it, there are four problem areas addressed:
- clear separation of business logic from other code (into dedicated modules)
- clear separation of code that handles different data sources (into dedicated modules)
- clear separation of code that handles different data sinks (into dedicated modules)
- appropriate structuring of compile-time dependencies between the above modules to make the code understandable and maintainable
The need for the first item isn’t particularly surprising; I think most software developers these days are convinced of the benefits of separating the code that implements business concepts (the “functional requirements”) from the other parts. Yes, there are plenty of code-bases that don’t do this but we are at least ashamed of them.
The usefulness of the second item is also reasonably accepted. If an application renders a web UI, but also has a REST interface, and accepts commands via a message-broker, then the code for each of those 3 mechanisms should be properly separated to make the system easier to understand and to maintain.
Item 3 is perhaps less thought-about. An application often writes to a database, a message-broker, and various systems via REST or various RPC protocols. Separating these different topics seems helpful - particularly if one of them gets replaced with an alternative, or additional external connection types are needed.
In a non-trivial-sized application, item 4 is needed to keep the system understandable, maintainable, and testable. Even when code is nicely separated into modules as described above, if compile-time dependencies are allowed from any module onto any module then the code will soon become very hard to understand and change. Testing any piece of it requires building the set of objects to test, and isolating some of them with test dummies (aka mocks) - something that becomes very hard when dependencies go in all directions.
The traditional solution to all this is the “layered architecture” in which a presentation-tier sits “above” a business-tier which sits “above” a persistence/infrastructure tier. Each tier is allowed to reference types declared in the tier below it, but not in the tier(s) above it, thus preventing circular dependencies between layers - which soon turns into a complicated web of dependencies. This layering can be enforced by convention, by tooling, or by placing each layer into a separate artifact (library) and controlling the compile-time dependencies between these artifacts.
There are, however, a few problems with the layered architecture.
The Problems with Layered Architecture
The “presentation tier” is just one conceptual blob, without addressing the fact that an application may have multiple kinds of “inputs” (external systems that initiate data transfers to it).
The persistence/infrastructure tier has the same problem - no prompt to developers to separate it further. However more importantly it has no access to types in the business tier; “DAO” classes cannot take model types as inputs or return them as outputs. Persistence frameworks are limited to general-purpose APIs like:
class Session {
void save(Object o);
Object fetchById(Class<?> type, Object id);
List<?> fetchByCriteria(Class<?> type, Criteria criteria);
}
The business-tier must then add type-safe wrappers over this general-purpose interface for different types. This pushes unnecessary code up into the business tier which really should be kept free of anything but business-relevant code.
This strategy of “type-unsafe” interfaces also doesn’t extend well to things other than persistence.
Rather than using type-unsafe underlying layers, an alternative sometimes adopted is to split the business tier into two parts: “model types” and “business services”, with the business services layer above the persistence/infrastructure layer (so it can invoke the necessary APIs) while the “business model” sits below the persistence/infrastructure so that helper code in that layer can reference those types. This however is (a) ugly and inconsistent, and (b) forces the model types to be “anemic” - ie plain data-holders with no significant functionality. That kills any useful “data-hiding” in those types, often makes it impossible for those model types to enforce invariants, and generally breaks the concepts of object-oriented development, domain-driven-design, and various other principles.
And finally, the layered architecture tempts developers to write business-tier code that depends on concrete types in the persistence/infrastructure layer - they are accessible after all. This can make it hard to swap out those technologies later, with the changes having impact on the important business logic. It also makes it hard to test the business logic alone; those “hard” dependencies can be difficult to mock.
How Hexagonal Helps
The primary principle is that a clear “business tier” should exist, and it should have no compile-time dependencies on any other code in the application. Any code that should be invoked by the outside world works as with the traditional layered architecture. However where business-tier code needs to initiate an interaction with the outside world, the business-tier declares an interface to support that operation, and then its code just calls that interface. The implementation is provided by some “adapter” module - ie code which has a compile-time dependency on the business tier rather than the reverse. The implementation of that interface of course needs to be provided to the business-tier somehow; this is done either by dependency-injection or some “service lookup” mechanism.
The concept of “ports and adapters” leads developers towards separating each input-mechanism and output-mechanism into a suitable module of some sort.
And because the business-tier has no dependencies on the application code, interacting only via special-purpose interfaces that it declares itself, the code in that tier is very isolated from the implementation. That implementation can obviously be replaced with anything for which that interface can be implemented. And unit-testing of the business code can always be done in isolation from the environment simply by providing dummy implementations of those interfaces.
It does have the disadvantage that code in the persistence/infrastructure layer now has transitive dependencies on everything that the business-tier code depends on - ie extends a weakness in the layered-architecture pattern that previously applied only to “input” code. While some languages allow “api” to be separated from “implementation” (eg for Java: OSGi or the Java Module System), this doesn’t seem very helpful here. The API for the business-tier is likely to involve most of the domain model types (entities and value objects in DDD terms), which is actually most of the code. Creating a separate “business tier API” consisting of DTO or anemic data model types doesn’t seem helpful or worthwhile. The problem of persistence/infrastructure code being exposed to these transitive dependencies is minor and it seems best to just accept that. It’s certainly a better problem to have than exposing the business tier to the transitive dependencies of the persistence/infrastructure tier which the layered architecture does..
The rule that business-tier code must not depend on application code should be enforced if possible. Using separate artifacts (eg maven modules) is effective; otherwise code-checking tools may help (eg jqassistant or archunit for Java).
Summary
Hexagonal Architecture, aka Ports and Adapters, is a technical code pattern which is applicable to applications which are of a non-trivial size and implement a significant amount of business logic. The pattern places business logic in modules that have no dependencies on other application code, with various “adapter” modules which surround it and depend on the business tier - calling it, or implementing interfaces to allow the adapter to be called.
It addresses the same problems that the “layered architecture” pattern does - but does it better.