Hexagonal Architecture - Ports and Adapters
How do we derive the Hexagonal Architecture diagram presented by Alistair Cockburn? How do we apply Hexagonal Architecture in designing a sample eCommerce system? What about the layers inside?
What is the essence of Hexagonal Architecture? Inversion of Control.
What are Driver Ports & Adapters? What are the Driven Ports & Adapters? What is the Application? What are the Dependencies?
Inversion of Control is the essence.
The essence behind Hexagonal Architecture is the Inversion of Control. To illustrate the difference:
In Traditional Layered Architecture, the Presentation Layer calls the Business Layer, and the Business Layer calls the Database Layer.
Translating this to Hexagonal Architecture: the Presentation Layer calls the user-side interface exposed by the Business Layer (and the Business Layer implements the user-side interface); furthermore, the Business Layer also declares (and calls) the data-side interface (which represents abstract access to persistence); lastly, the Database Layer implements the data-side interface (e.g. ORM, SQL). But, more formally (since we will *not* use the word "layers") - on the user-side, the Driver Adapters call the Driver Ports, and on the server-side, the Driven Adapters implement the Driven Ports. The Application exposes both Driver Ports & Driven Ports.
From this, we can see that the essence of Hexagonal Architecture is that it breaks direct dependencies. So, the Presentation Layer does NOT depend on the Business Layer implementation. The Business Layer does NOT depend on the Database Layer implementation.
For a more formal overview, see Alistair Cockburn’s Hexagonal Architecture article.
Driver Ports
Examples of Driver Ports are:
Order Service Interface, exposing methods: Submit Order, Cancel Order, View Order Details
Product Service Interface, exposing methods: Sync Product Pricing, Get Product Details
We can write Unit Tests targeting Driver Ports to test use case / business logic.
Driver Adapters
Examples of Driver Adapters are:
Tests
REST API
SOAP API
GraphQL API
gRPC API
WebSockets API
RabbitMQ Consumer
Kafka Consumer
Job Scheduler
SPA App
Desktop App
Mobile App
MVC App
CLI App
FTP Server App
The Driver Adapters are dependent on the Driver Ports, more specifically they call/use the Driver Ports. For example:
The Test Adapter calls the Order Service Interface methods and asserts expected results
The REST API adapter has an Order Controller which is dependent on the Order Service Interface. The Order Controller has an
HTTP POST
methodapi/orders
which accepts some JSON request and uses that to call the Order Service Interface, method Submit Order, and then returns some HTTP status code with JSON response. Thus, we can see that the REST API Order Controller is just a thin HTTP wrapper that delegates to the Order Service Interface.Similarly, other Driver Adapters (might be HTTP adapters, or UI adapters, or anything else) delegate work to the Driver Ports
We can test the Driver Adapters through Integration Tests by “mocking out“ the Driver Ports.
Application
Previously, we listed examples of Driver Ports:
Order Service Interface, exposing methods: Submit Order, Cancel Order, View Order Details
Product Service Interface, exposing methods: Sync Product Pricing, Get Product Details
The Application (the Hexagon) externally exposes the Driver Ports, i.e. the Driver Ports are a facade to the Application. The Application internally implements the Driver Ports. Using the example above, the implementations could be:
Order Service Implementation implements the Order Service Interface
Product Service Implementation implements the Product Service Interface
It should be noted that Hexagonal Architecture does NOT prescribe any layering inside the Application itself (i.e., inside the Hexagon). This was also summarized by Juan Manuel Garrido de Paz:
Ports & Adapters pattern says nothing about the structure of the inside of the hexagon. You can have layers… you can have components by feature… you can have spaghetti code… you can have a Big Ball of Mud… you can apply DDD tactical patterns… you can have a single CRUD… it’s up to you.
This means that, in our example, we do not have any prescription regarding how Order Service Implementation and Product Service Implementation will be done.
Driven Ports
The Application implementation may need to access the external world - but how do we avoid coupling it to infrastructural concerns?
For example, Order Service Implementation is implementing the method Submit Order. To implement that method, we need to perform the following:
Check that the Products exist (so we need to retrieve information about products from some storage mechanism)
Generate a unique order ID
Create the Order, and calculate the order price by taking into account Product pricing, as well as discounts which are dependent on the day of the week
Persist the Order in some persistence mechanism
Authorize payment for the Order, and update the order status to determine whether the payment was successful or not
Contact the shipping mechanism so that shipping can be started
Publish an event that the Order was submitted
So, we may have the following Driven Ports, to abstract away I/O and sources of non-determinism. I/O abstraction is an essential aspect of Hexagonal Architecture. Non-determinism abstraction is needed for unit testability.
The following Driven Ports abstract away I/O concerns:
Order Repository Interface - this is an abstraction over the persistence mechanism (we don’t care which database is used or whether we use ORM or raw SQL or any other implementation)
Product Repository Interface - this is an abstraction over the persistence mechanism (once again, we don’t care about concrete database implementation)
Order Notifier Interface - this is an abstraction over some messaging publishing (we don’t care about which message broker we’ll use)
Payment Gateway Interface - this is an abstraction over the payment system that we’ll be using for payments
Shipping Gateway Interface - this is an abstraction over the shipping system we’ll use
The following Driven Ports abstract away sources of non-determinism:
Order ID Generator Interface - this is an abstraction over mechanism for generating random Order ID numbers
Time Provider Interface - this is an abstraction over system time
In this way, the Application is decoupled from both I/O and non-determinism. It means that we are able to unit-test Driver Ports, by substituting Driven Ports with Test Doubles.
Driven Adapters
The following are examples of Driven Adapters for the following ports (we list the Driven Ports, followed by their Driven Adapters):
Order Repository Interface
Fake Repository Implementation
SQL DB Order Repository Implementation (ORM)
SQL DB Order Repository Implementation (ORM Lite)
MongoDB Order Repository Implementation
Redis Order Repository Implementation
Cassandra Order Repository Implementation
Neo4J Order Repository Implementation
File Order Repository Implementation
Amazon S3 Order Repository Implementation
Azure Blob Order Repository Implementation
Order Notifier Interface
Fake Order Notifier Implementation
RabbitMQ Order Notifier Implementation
Kafka Order Notifier Implementation
Payment Gateway Interface
Fake Payment Gateway Implementation
PayPal Payment Gateway Implementation
Stripe Payment Gateway Implementation
Shipping Gateway Interface
Fake Shipping Gateway Implementation
DHL Shipping Gateway Implementation
ABC Shipping Gateway Implementation
Order ID Generator Interface
Fake Order ID Generator Implementation
UUID Order ID Generator Implementation
ULID Order ID Generator Implementation
Time Provider Interface
Fake Time Provider Implementation
System Time Provider Implementation
Note: You will see a “Fake“ Adapter in each of the above. A Fake is one type of Test Double. Other Test Doubles may be used instead. The choice of which Test Doubles we’ll use is outside of the scope of Hexagonal Architecture.
We can see above that with Adapters, we can implement I/O integration (e.g. which database will be used to implement the Order Repository Interface, which message broker will be used to implement the Order Notifier Interface, which REST/SOAP/etc. API will be used to implement the Payment Gateway Interface, etc.) Thus, we’re implementing any I/O concerns, e.g. disk access and any network communication.
Furthermore, we can see that with the Adapters we can implement any non-deterministic mechanisms. For example, access to the system time as well as using third-party libraries for random number generation.
Each Driven Port will have at least one adapter - the “Test Double” Adapter. There can be one or more real adapters, e.g. we may have MongoDB Order Repository Implementation for the Order Repository Interface; and for Payment Gateway Interface, perhaps we could have two simultaneous implementations (e.g. Payment Gateway Implementation, Stripe Payment Gateway Implementation).
Sometimes, we can implement a real adapter right away (e.g. the DHL Shipping Gateway Implementation because DHL already has some REST API), but sometimes the real adapter implementation will need to be deferred for months (e.g. the ABC Shipping Gateway Implementation cannot be implemented right now, but we need to wait for months because it’s being developed by some external company ABC).
Regardless of when real adapters are implemented (whether soon or deferred until later), we are NOT blocked because we have fake adapters. The fake adapters can be used for running the Application, and for Unit Testing the Application.
We can write Integration Tests targeting Driven Ports. So we write some Integration Tests for a Driven Port, and then at run time, those tests can be executed for every Driven Adapter for that Driven Port.
Interface vs Implementation Details
From the perspective of Hexagonal Architecture, let’s see what are the Interfaces vs Implementation Details.
The “interfaces” are the following:
Driver Ports are interfaces for the use cases
Driven Ports are interfaces for persistence, messaging, etc.
The “implementation (details)” are the following:
Driver Adapters are the implementation of presentation concerns; they use Driver Ports.
Driven Adapters are the implementation of Driven Ports.
Application (internals) is the implementation of the Driver Ports. So whether we use Transaction Script, or have Application Services + Domain Model, whether we have a Rich Domain or Anemic Domain, or any other choices for implementation the Application - all these are implementation details.
YouTube Live Streaming
You can view the full live streaming session recording here, as we went step by step in reaching the Hexagonal Architecture Diagram, as well as the examples above for the Ports & Adapters involved in the Pizza eShop case study - watch on YouTube:
For our next session, we plan to do some live coding to demonstrate Hexagonal Architecture practically. Subscribe to my YouTube channel to get updates.
Wow, excellent post! Direct to the point and very educational. Thanks for sharing!