Understanding hexagonal architecture
“Create your application to work without either a UI or a database so you can run automated regression-tests against the application, work when the database becomes unavailable, and link applications together without any user involvement.”
– Alistair Cockburn.
That quote lays the ground for understanding hexagonal architecture. We can go even further with Cockburn’s idea and make our application work without any technology, not just the ones related to the UI or database.
One of the main ideas of hexagonal architecture is to separate business code from technology code.Not just that, we must also make sure the technology side depends on the business one so that the latter can evolve without any concerns regarding which technology is used to fulfill business goals. Having the business logic independent of any technology details gives a system the flexibility to change technologies without disrupting its business logic. In that sense, the business logic represents the foundation through which the application is developed and from which all other system components will derive.
We must be able to change technology code without causing harm to its business counterpart. To achieve this, we must determine a place where the business code will exist, isolated and protected from any technology concerns. It’ll give rise to the creation of our first hexagon: the Domain hexagon.
In the Domain hexagon, we assemble the elements responsible for describing the core problems we want our software to solve. Entities and value objects are the main elements utilized in the Domain hexagon. Entities represent things we can assign an identity to, and value objects are immutable components that we can use to compose our entities. The meaning this book uses for entities and value objects comes from domain-driven design principles.
We also need ways to use, process, and orchestrate the business rules coming from the Domain hexagon. That’s what the Application hexagon does. It sits between the business and technology sides, serving as a middleman to interact with both parts. The Application hexagon utilizes ports and use cases to perform its functions. We will explore those things in more detail in the next section.
The Framework hexagon provides the outside-world interface. That’s the place where we have the opportunity to determine how to expose application features – this is where we define REST or gRPC endpoints, for example. To consume things from external sources, we use the Framework hexagon to specify the mechanisms to fetch data from databases, message brokers, or any other system. In the hexagonal architecture, we materialize technology decisions through adapters. The following diagram provides a high-level view of the architecture:
Figure 1.4 – The hexagonal architecture
Next, we’ll go deeper into the components, roles, and structures of each hexagon.
Domain hexagon
The Domain hexagon represents an effort to understand and model a real-world problem. Suppose you’re working on a project that requires creating a network and topology inventory for a telecom company. This inventory’s main purpose is to provide a comprehensive view of all resources that comprise the network. Among those resources, we have routers, switches, racks, shelves, and other equipment types. Our goal here is to use the Domain hexagon to model into code the knowledge required to identify, categorize, and correlate those network and topology elements and provide a lucid and organized view of the desired inventory. That knowledge should be, as much as possible, represented in a technology-agnostic form.
This quest is not a trivial one. Developers involved in such an undertaking may not know much about the telecom business, set aside this inventory thing. As recommended by Domain-Driven Design: Tackling Complexity in the Heart of Software, domain experts or other developers who already know the problem domain should be consulted. If none are available, you should try to fill the knowledge gap by searching in books or any other material that teaches about the problem domain.
Inside the Domain hexagon, we have entities corresponding to critical business data and rules. They are critical because they represent a model of the real problem. That model may take some time to evolve and reflect consistently on the problem domain. That’s often the case with new software projects where neither developers nor domain experts have a clear vision of the system’s purpose in its early stages. In such scenarios, particularly recurrent in start-up environments, it’s normal and predictable to have an initial awkward domain model that evolves only as business ideas also evolve and are validated by users and domain experts. It’s a curious situation where the domain model is unknown even to the so-called domain experts.
On the other hand, in scenarios where the problem domain exists and is clear in the minds of domain experts, if we fail to grasp that problem domain and how it translates into entities and other domain model elements, such as value objects, we risk building our software based on weak or wrong assumptions.
Weak assumptions can be one of the reasons why software may start simple but, as its code base grows, it accumulates technical debt and becomes harder to maintain. These weak assumptions may lead to fragile and unexpressive code that can initially solve business problems but is not ready to accommodate changes in a cohesive way. Bear in mind that the Domain hexagon is composed of whatever kind of object categories you feel are good for representing the problem domain. Here is a representation based just on entities and value objects:
Figure 1.5 – Domain hexagon
Let’s now talk about the components comprising this hexagon.
Entities
Entities help us to build more expressive code. What characterizes an entity is its sense of continuity and identity, as described by Domain-Driven Design: Tackling Complexity in the Heart of Software. That continuity is related to the life cycle and mutable characteristics of the object. For example, in our network and topology inventory scenario, we mentioned the existence of routers. For a router, we can define whether its state is enabled or disabled.
Also, we can assign some properties describing the relationship that a router has with different routers and other network equipment. All those properties may change over time, so we can see that the router is not a static thing and its characteristics inside the problem domain can change. Because of that, we can state that the router has a life cycle. Apart from that, every router should be unique in an inventory, so it must have an identity. So, continuity and identity are the elements that determine an entity.
The following code shows a Router
entity class composed of RouterType
and RouterId
value objects:
//Router entity class public class Router { private final Type type; private final RouterId id; public Router(Type type, RouterId id) { this.type = type; this.id = id; } public static List<Router> checkRouter( Type type, List<Router> routers) { var routersList = new ArrayList<Router>(); routers.forEach(router -> { if(router.type == type ){ routersList.add(router); } }); return routersList; } }
Value objects
Value objects complement our code’s expressiveness when there is no need to identify something uniquely, as well as when we are more concerned about the object’s attributes than its identity. We can use value objects to compose an entity object, so we must make them immutable to avoid unforeseen inconsistencies across the domain. In the router example presented previously, we can represent the Type
router as a value object attribute from the Router
entity:
public enum Type { EDGE, CORE; }
Application hexagon
So far, we’ve been discussing how the Domain hexagon encapsulates business rules with entities and value objects. But there are situations where the software does not need to operate directly at the domain level. Clean Architecture: A Craftsman’s Guide to Software Structure and Design states that some operations exist solely to allow the automation provided by the software. These operations – although they support business rules – would not exist outside the context of the software. We’re talking about application-specific operations.
The Application hexagon is where we abstractly deal with application-specific tasks. I mean abstract because we’re not dealing directly with technology concerns yet. This hexagon expresses the software’s user intent and features based on the Domain hexagon’s business rules.
Based on the same topology and inventory network scenario described previously, suppose you need a way to query routers of the same type. It would require some data handling to produce such results. Your software would need to capture some user input to query for router types. You may want to use a particular business rule to validate user input and another business rule to verify data fetched from external sources. If no constraints are violated, your software then provides the data showing a list of routers of the same type. We can group all those different tasks in a use case. The following diagram depicts the Application hexagon’s high-level structure based on use cases, input ports, and output ports:
Figure 1.6 – Application hexagon
The following sections will discuss the components of this hexagon.
Use cases
Use cases represent a system’s behavior through application-specific operations that exist within the software realm to support the domain’s constraints. Use cases may interact directly with entities and other use cases, making them quite flexible components. In Java, we represent use cases as abstractions defined by interfaces expressing what the software can do. The following example shows a use case that provides an operation to get a filtered list of routers:
public interface RouterViewUseCase { List<Router> getRouters(Predicate<Router> filter); }
Note the Predicate
filter. We’re going to use it to filter the router list when implementing that use case with an input port.
Input ports
If use cases are just interfaces describing what the software does, we still need to implement the use case interface. That’s the role of the input port. By being a component that’s directly attached to use cases, at the Application level, input ports allow us to implement software intent on domain terms. Here is an input port providing an implementation that fulfills the software intent stated in the use case:
public class RouterViewInputPort implements RouterViewUse Case { private RouterViewOutputPort routerListOutputPort; public RouterViewInput Port(RouterViewOutputPort routerViewOutputPort) { this.routerListOutputPort = routerViewOutputPort; } @Override public List<Router> getRouters(Predicate<Router> fil ter) { var routers = routerListOutput Port.fetchRouters(); return Router.retrieveRouter(routers, filter); } }
This example shows us how we could use a domain constraint to make sure we’re filtering the routers we want to retrieve. From the input port’s implementation, we can also get things from outside the application. We can do that using output ports.
Output ports
There are situations in which a use case needs to fetch data from external resources to achieve its goals. That’s the role of output ports, which are represented as interfaces describing, in a technology-agnostic way, what kind of data a use case or input port would need to get from outside to perform its operations. I say agnostic because output ports don’t care whether the data comes from a particular relational database technology or a filesystem, for example. We assign this responsibility to output adapters, which we’ll look at shortly:
public interface RouterViewOutputPort { List<Router> fetchRouters(); }
Now, let’s discuss the last type of hexagon.
Framework hexagon
Things seem well organized with our critical business rules constrained to the Domain hexagon, followed by the Application hexagon dealing with some application-specific operations through the means of use cases, input ports, and output ports. Now comes the moment when we need to decide which technologies should be allowed to communicate with our software. That communication can occur in two forms, one known as driving and the other as driven. For the driver side, we use input adapters, and for the driven side, we use output adapters, as shown in the following diagram:
Figure 1.7 – Framework hexagon
Let’s look at this in more detail.
Driving operations and input adapters
Driving operations are the ones that request actions from the software. It can be a user with a command-line client or a frontend application on behalf of the user, for example. There may be some testing suites checking the correctness of things exposed by your software. Or it could just be other applications in a large ecosystem needing to interact with some exposed software features. This communication occurs through an Application Programming Interface (API) built on top of the input adapters.
This API defines how external entities will interact with your system and then translate their request to your domain’s application. The term driving is used because those external entities are driving the behavior of the system. Input adapters can define the application’s supported communication protocols, as shown here:
Figure 1.8 – Driver operations and input adapters
Suppose you need to expose some software features to legacy applications that work just with SOAP over HTTP/1.1 and, at the same time, need to make those same features available to new clients who could leverage the advantages of using gRPC over HTTP/2. With the hexagonal architecture, you could create an input adapter for both scenarios, with each adapter attached to the same input port, which would, in turn, translate the request downstream to work in terms of the domain. Here is an input adapter using a use case reference to call one of the input port operations:
public class RouterViewCLIAdapter { private RouterViewUseCase routerViewUseCase; public RouterViewCLIAdapter(){ setAdapters(); } public List<Router> obtainRelatedRouters(String type) { RelatedRoutersCommand relatedRoutersCommand = new RelatedRoutersCommand(type); return routerViewUseCase.getRelatedRouters (relatedRoutersCommand); } private void setAdapters(){ this.routerViewUseCase = new RouterViewInputPort (RouterViewFileAdapter.getInstance()); } }
This example illustrates the creation of an input adapter that gets data from STDIN. Note the use of the input port through its use case interface. Here, we passed the command that encapsulates input data that’s used on the Application hexagon to deal with the Domain hexagon’s constraints. If we want to enable other communication forms in our system, such as REST, we just have to create a new REST adapter containing the dependencies to expose a REST communication endpoint. We will do this in the following chapters as we add more features to our hexagonal application.
Driven operations and output adapters
On the other side of the coin, we have driven operations. These operations are triggered from your application and go into the outside world to get data to fulfill the software’s needs. A driven operation generally occurs in response to some driving one. As you can guess, the way we define the driven side is through output adapters. These adapters must conform to our output ports by implementing them. Remember, an output port tells the system what kind of data it needs to perform some application-specific task. It’s up to the output adapter to describe how it will get the data. Here is a diagram of output adapters and driven operations:
Figure 1.9 – Driven operations and output adapters
Suppose your application started working with Oracle relational databases and, after a while, you decided to change technologies and move on to a NoSQL approach, embracing MongoDB instead as your data source. In the beginning, you’d have just one output adapter to allow persistence with Oracle databases. To enable communication with MongoDB, you’d have to create an output adapter on the Framework hexagon, leaving the Application and, most importantly, Domain hexagons untouched. Because both input and output adapters are pointing inside the hexagon, we’re making them depend on both Application and Domain hexagons, hence inverting the dependency.
The term driven is used because those operations are driven and controlled by the hexagonal application itself, triggering action in other external systems. Note, in the following example, how the output adapter implements the output port interface to specify how the application is going to obtain external data:
public class RouterViewFileAdapter implements Router ViewOutputPort { @Override public List<Router> fetchRouters() { return readFileAsString(); } private static List<Router> readFileAsString() { List<Router> routers = new ArrayList<>(); try (Stream<String> stream = new BufferedReader( new InputStreamReader( Objects.requireNonNull( RouterViewFileAdapter.class .getClassLoader(). getResourceAsStream ("routers.txt")))).lines()) { stream.forEach(line ->{ String[] routerEntry = line.split(";"); var id = routerEntry[0]; var type = routerEntry[1]; Router router = new Router (RouterType.valueOf(type) ,RouterId.of(id)); routers.add(router); }); } catch (Exception e){ e.printStackTrace(); } return routers; } }
The output port states what data the application needs from outside. The output adapter in the previous example provides a specific way to get that data through a local file.
Having discussed the various hexagons in this architecture, we will now look at the advantages that this approach brings.
Advantages of the hexagonal approach
If you’re looking for a pattern to help you standardize the way software is developed at your company or even in personal projects, hexagonal architecture can be used as the basis to create such standardization by influencing how classes, packages, and the code structure as a whole are organized.
In my experience of working on large projects with multiple vendors and bringing lots of new developers to contribute to the same code base, the hexagonal architecture helps the organization establish the foundational principles on which the software is structured. Whenever a developer switched projects, they had a shallow learning curve to understand how the software was structured because they were already acquainted with hexagonal principles they’d learned about in previous projects. This factor, in particular, is directly related to the long-term benefits of software with a minor degree of technical debt.
Applications with a high degree of maintainability that are easy to change and test are always welcomed. Let’s see next how hexagonal architecture helps us to obtain such advantages.
Change-tolerant
Technology changes are happening at a swift pace. New programming languages and a myriad of sophisticated tools are emerging every day. To beat the competition, very often, it’s not enough to just stick with well-established and time-tested technologies. The use of cutting-edge technology becomes no longer a choice but a necessity, and if the software is not prepared to accommodate such changes, the company risks losing money and time on big refactoring because the software architecture is not change-tolerant.
So, the port and adapter features of hexagonal architecture give us a strong advantage by providing the architectural principles to create applications that are ready to incorporate technological changes with less friction.
Maintainability
If it’s necessary to change some business rule, we know that the only thing that should be changed is the Domain hexagon. On the other hand, if we need to allow an existing feature to be triggered by a client that uses particular technology or protocol that is not yet supported by the application, we just need to create a new adapter, performing this change only on the Framework hexagon.
This separation of concerns seems simple, but when enforced as an architectural principle, it grants a degree of predictability that’s enough to decrease the mental overload of grasping the basic software structures before deep diving into its complexities. Time has always been a scarce resource, and if there’s a chance to save it through an architectural approach that removes some mental barriers, I think we should at least try it.
Testability
One of the hexagonal architecture’s ultimate goals is to allow developers to test the application when its external dependencies are not present, such as its UI and databases, as Alistair Cockburn stated. This does not mean, however, that this architecture ignores integration tests. Far from it – instead, it allows a more loosely coupled approach by giving us the required flexibility to test the most critical part of the code, even in the absence of dependencies such as databases.
By assessing each of the elements comprising the hexagonal architecture and being aware of the advantages such an architecture can bring to our projects, we’re now equipped with the fundamentals to develop hexagonal applications.