The overarching goal of Hexagonal architecture is to isolate the business logic from the infrastructure concerns1. Infrastructure refers to the specific technology dependencies we choose to adopt, e.g. database, presentation, libraries. This separation of concerns enables flexibility2 if and when we need adjust our technology choices, e.g. to scale, improve security, support new features, etc.
Hexagonal architecture3 comes in a few flavors, but from my perspective, they all try to achieve the aforementioned goal. Some of these flavors may be familiar, e.g. Onion architecture4, Ports and Adapters, Clean architecture5, Functional Core and Imperative Shell6.
Structure
This is my simplified interpretation of the architecture. So what do the concentric circles mean are what are the layers responsible for?
Dependency Rule
The circles look like an onion7 in order to reinforce the fundamental rule that each layer can only depend on what is inside of it8. Put literally, Domain can not have depedencies on the Application or Infrastructure; Application depends on Domain but not Infrastructure; and Infrastructure depends on both the Domain and Application.
This rule helps us mitigate leaky abstractions, where if we want implementation details to leak through, it has to be explicitly exposed in the Application or Domain. For example, a choice to use cursor over offset pagination9 will manifest itself as a cursor
or offset
argument in a Application service.
Importantly, the 3rd party libraries the domain adopts will become dependencies of the parent layers - so typically it is in our best interest to keep the domain dependency free.
Domain
The Domain contains our entities10. For example, for an ecommerce app, this may include Product
, Order
, Customer
, etc. It can also include any exceptions that could arise from interacting with these entities, e.g. InsufficientStock
.
When adding entities and logic to the Domain, it can be helpful to think in the context of a larger system where other apps could consume our entities11. We can ask ourselves, is this entity or logic widely shareable? For example, in order to interact with our app, consumers need to build a client service on their end, it would be awesome to provide them with the types to use in their client, better yet, define a client interface for them.
Application
The Application implements the use cases12 of the app. For example, a Customer
can create an Order
for a Product
; we can add/remove/update a Product
; a Customer
can search through the Product
(s).
Services
In order to achieve the use cases, we will likely need the services with the capability to save and retrieve our entities and to communicate with other apps (e.g. payment processors). Instead of implementing a service for saving/retrieving a Product
from PostgreSQL, we should define an interface for what capabilities we need - e.g. we may need to saveProduct
and getProductById
, which would be supported by most databases; but a capability like searchProducts
would likely influence what we choose, e.g. ElasticSearch or even a multi-database strategy to support all these capabilities.
This process of defining the interface before implementation, enables us to delay the technology decisions until we have a solid understanding of the requirements.
Most languages support the concept of an interface131415.
Handlers
The use case can be defined as a single function that takes a request and any dependent services in order to return a response - I call this a handler. The handler is agnostic to how it is called, whether that be from a REST API or GraphQL, this is an infrastructure concern.
For example, to enable a Customer
to create an Order
for a Product
, we would have a CreateOrderRequest
that would include the CustomerId
and ProductIds
and on completion, return us a CreateOrderResponse
with an OrderId
in it. It may depend on services like a CustomerService
for getting the customer address, ProductService
for verifying if there is any stock left and an OrderService
for saving the order and generating an OrderId
.
import { CustomerId, ProductId, OrderId } from "domain";
type CreateOrderRequest = {
customerId: CustomerId;
productIds: ProductId[];
};
type CreateOrderResponse = {
orderId: OrderId;
};
const createOrder = (
request: CreateOrderRequest,
dependencies: {
customerService: CustomerService;
productService: ProductService;
}
): CreateOrderResponse => {
// ...
};
Infrastructure
This is where our app meets the real world. We need to make technology choices. We need to implement the service interfaces and wire the handlers to some kind of runtime.
Implementing services
Our Application layer defines an OrderService
interface, a specific provider would be PostgreSQLOrderService
, GraphQLOrderService
or ShopifyOrderService
- as we can see there is flexibility in what technology we choose.
The implementation and testing of a service could involve its building out its own Hexagonal architecture, given it is complex enough. For example, serializing an entity, e.g. Product
, to tables in a SQL database could involve mapping to a ProductDao
(data access object16) that may require dates in a specific format (e.g ISO-8601 string), booleans in bits, or additional fields that represent columns for indexing purposes - all database concerns. There could also be complexity where a single Product
may need to saved to multiple databases, one as a transactional record (PostgreSQL) and the other for search (ElasticSearch).
Wiring up handlers
Handlers are agnostic to how they are called. Wiring them to a runtime will require mapping the inputs to the request and the response to the outputs.
For example, if we had a REST API, we will need to decide whether the inputs are exposed through the HTTP body
, query
and/or path
parameters; how do we output errors? e.g. when to return 400
s and 500
s; how will the output be formatted in the body
, do we use JSON?, etc.
Runtimes are not limited to APIs (GraphQL, REST, gRPC), they can also be console apps, queue processors, etc.
Example
Footnotes
-
https://alistair.cockburn.us/hexagonal-architecture/#:~:text=The%20application%20has%20a%20semantically%20sound%20interaction%20with%20the%20adapters%20on%20all%20sides%20of%20it%2C%20without%20actually%20knowing%20the%20nature%20of%20the%20things%20on%20the%20other%20side%20of%20the%20adapters. ↩
-
https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749 ↩
-
https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/#:~:text=Alistair%20Cockburn%20has%20written%20a%20bit%20about%20Hexagonal%20architecture.%C2%A0%20Hexagonal%20architecture%20and%20Onion%20Architecture%20share%20the%20following%20premise%3A%C2%A0%20Externalize%20infrastructure%20and%20write%20adapter%20code%20so%20that%20the%20infrastructure%20does%20not%20become%20tightly%20coupled. ↩
-
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html ↩
-
https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/ ↩
-
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#:~:text=single%20actionable%20idea.-,The%20Dependency%20Rule,-The%20concentric%20circles ↩
-
https://slack.engineering/evolving-api-pagination-at-slack/ ↩
-
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#:~:text=the%20inner%20circles.-,Entities,-Entities%20encapsulate%20Enterprise ↩
-
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#:~:text=It%20doesn%E2%80%99t%20matter,of%20the%20application. ↩
-
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html#:~:text=the%20entity%20layer.-,Use%20Cases,-The%20software%20in ↩
-
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface ↩