Horizontal Architecture vs Vertical Architecture
Hexagonal Architecture doesn't prescribe layers. So how do we structure the internals of a Hexagon. How do we compare "Horizontal" Concentric Architectures with "Vertical" Slice Architecture?
When we’re designing a monolith, designing a module within a modular monolith, or designing a microservice, we ask ourselves the question - which architecture should we use? The overall split is:
Horizontal Architecture
Flat - Traditional Layered App
Concentric - Hexagonal, Onion, Clean
Vertical Architecture
How can we implement the Hexagon? (1) Transaction Script with Anemic Domain or (2) Service Layer with Rich Domain?
What could the layers inside the Hexagon be?
Fat Application Layer & Anemic Domain Model: We can use the Transaction Script pattern (with interfaces for infrastructure) to model logic in a procedural way, and our anemic domain model would be just plain data structures.
Thin Application Layer & Rich Domain Model: We can use the Service Layer Pattern to implement application logic, whereas we place business logic inside the Domain Model
How coarse are the Driver Ports?
Coarse - Application Services: We can model our use case interfaces in a coarse grained way (group use cases)
Granular - Request Handlers: We can model our use case interfaces in a granular way (separate each use case)
Horizontal (Layer) Architecture
We have two types of “Horizontal“ Architectures:
Traditional Layered Architecture - Flat Layers (Presentation Layer, depending on Business Logic Layer, depending on Data Layer)
Concentric Architectures - Hexagonal Architecture, Onion Architecture, Clean Architecture (Business Logic is independent of Presentation/Infrastructure)
We can switch from traditional Horizontal 3-layer architecture to Hexagonal Architecture by applying the Dependency Inversion Principle.
Hexagonal Architecture only prescribes the separation between the Application and the External World (i.e., the separation between Application & Presentation/Infrastructure concerns; separation between business logic vs. I/O concerns), but it does NOT specify any rules regarding its internal structure - there is no specification of layers inside the Hexagon (nor even whether or not to use layers inside the Hexagon).
So, how do we structure the internals of the Hexagon, i.e., how do we implement the Driver Ports inside the Hexagon?
Please note - in the following table and drawings, we are focused exclusively on the Hexagon (Application), not the Adapters. Thus, we are considering the layers involved in implementing the Hexagon (Application).
Hexagon Implementation - Layer Approach
As was described in the section “Application“ above, Hexagonal Architecture does NOT prescribe the layers inside the application, i.e., how we’ll organize the implementation of logic inside.
The following are some approaches how we could structure the internals of the Application (Hexagon).
Let’s recall one of our Driver Ports from the article Hexagonal Architecture - Ports and Adapters:
Order Service Interface
, exposing methods:Submit Order
,Cancel Order
,View Order Details
How do we implement it? Let’s look at two well-known approaches below (or you may use any other approach, there is no restriction).
Layering Approach 1: Transaction Script
Transaction Script prescribes that all logic for some command is placed within a single procedure.
So given the Driver Port:
Order Service Interface
, exposing methods:Submit Order
,Cancel Order
,View Order Details
The implementation of that Driver Port would be:
Order Service Implementation class
, which implementsOrder Service Interface
So this means when we see Order Service Implementation
and look at the source code for Submit Order,
we would see that the method is “fat“, it is doing everything needed to submit the order, including creating the order data structure, calculating the total price based on product pricing and special discount logic, and calling the database to save the order. We use a procedural programming style here, i.e., the Submit Order
method is effectively a procedure.
Note: Regarding the sentence “calling the database to save the order“ - Transaction Script pattern enables us to call the database directly or via thin database wrapper. For the purposes of its usage within Hexagonal Architecture, we would be making calls to the database via the Persistence Driven Ports (which has a corresponding implementation - a thin database wrapper).
Layering Approach 2: Service Layer + Domain Model
The transaction Script approach described above might be “ok” with simple logic, but as we’ve seen above, it can become unmaintainable when the business logic becomes more complex. So instead of putting ALL logic inside the Transaction Script, how about we split it?
Let’s introduce layers and split logic as followings:
Service Layer, for modeling application logic
Domain Layer, for modeling business logic
Both of these are described in Fowler’s book
So, back to our example, given the Driver Port:
Order Service Interface
, exposing methods:Submit Order
,Cancel Order
,View Order Details
The implementation of that Driver Port would be:
In the Service Layer, we’d have
Order Service Implementation class
, which implementsOrder Service Interface
In the Domain Layer, we’d have the
Order
domain entity.
Let’s see how we split logic between those two classes:
Order Service Implementation class
, methodSubmit Order
contains the application logic, e.g., making a call to theOrder
constructor, retrieving producing pricing from the database (via a Driven Port), and saving the Order to the database (via a Driven Port). You will notice that there is NO business logic in theSubmit Order
method!Order class
contains business logic, such as the logic for calculating order pricing, applying special discount logic to get discounted pricing, etc.
Driver Ports - Granularity
In the section “Application“ we modeled Driver Ports as follows, see the article Hexagonal Architecture - Ports and Adapters:
Order Service Interface, exposing methods: Submit Order, Cancel Order, View Order Details
Product Service Interface, exposing methods: Sync Product Pricing, Get Product Details
However, there is no prescription on how coarse or granular the ports are, so any of these would have been fine (listed in coarseness):
Driver Port Approach 1: Application Services
Order Service Interface, exposing methods: Submit Order, Cancel Order, View Order Details
Product Service Interface, exposing methods: Sync Product Pricing, Get Product Details
Driver Port Approach 2: Request Handlers
Submit Order Interface, exposing method: Handle
Cancel Order Interface, exposing method: Handle
View Order Details Interface, exposing method: Handle
Sub Product Pricing Interface, exposing method: Handle
Get Product Details Interface, exposing method: Handle
Both Transaction Script & Service Layer approaches are still applicable.
Onion Architecture & Clean Architecture
As we model the Application (Hexagon), we can structure it as follows:
In Onion Architecture, the Application is structured using layers: Application Services + Domain Services + Domain Model. In our taxonomy above, it corresponds to:
Layering Approach 2: Service Layer + Domain Model. (In Onion Architecture, the Service Layer is implemented with Application Services. The Domain Model is implemented with Domain Services & Domain Model).
Driver Port Approach 2: (Application Services). (In Onion Architecture, the Application Services are the entry points to the Application).
In Clean Architecture, the Application is structured using layers: Use Cases + Entities. This is equivalent to applying both of these:
Layering Approach 2: Service Layer + Domain Model. (In Clean Architecture, the Service Layer is implemented in a fine-grained way, with Use Cases, which are implemented with the Command Handler pattern. The Domain Model is referred to as Entities in Clean Architecture)
Driver Port Approach 3: (Command Handlers). (In Clean Architecture, the Use Cases “scream“ at us. The Use Cases, implemented as Command Handlers, are the entry points to the Application).
As we model the Adapters, we can structure it as follows:
Driver Adapters: User Interface, Tests
Driven Adapters: Infrastructure
In Clean Architecture: (Interface Adapters Layer)
Driver Adapters: Controllers, Presenters, [Tests]
Driven Adapters: Gateways
Note: I added the [Tests] in Driver Adapters in Clean Architecture, because even though it doesn’t appear directly in Uncle Bob’s diagram, the Tests are implemented in his Clean Coders videos targeting the Application via the Use Cases.
Vertical (Slice) Architecture
In the Vertical approach, we don't separate by Layers (Presentation, Business Logic, Infrastructure); instead, we split by Requests, whereby each Request represents a distinct Use Case. But doesn't this idea already exist in Clean Architecture? Yes, it does, except in the case of Vertical Slice, there aren't any layers. So this means when implementing Request processing, for some Request, we may choose to directly talk to the database; in another Request, we may choose to use an ORM, in yet another Request, we may choose to work with a rich domain (DDD), etc. Thus, we do not have some generally shared repository interface, unlike in Clean Architecture.
Vertical Slice Architecture also works well with CQRS, where we model each Request as a Command or Query. The Command Handler may (or may not) involve working with a Rich Domain. The Query handler will typically involve just returning DTOs, i.e., projects of data; there is no Rich Domain. So we can see in this way that we have a separate "model" for the Write (Command) versus Read (Side). We may have a single database (used both for Reads and Writes) or two databases (one for Reads, the other for Writes, kept in sync - e.g., using events)