How do we transform a traditional 3-layer architecture into Hexagonal Architecture (Ports & Adapters)?
Traditional 3-Layer architecture:
Presentation:
ChallengeController
Business Logic:
ChallengeService
Infrastructure:
ChallengeRepository, SystemTime
Hexagonal Architecture (Ports & Adapters):
DRIVER ADAPTERS - Presentation:
ChallengeController
APPLICATION
DRIVER PORTS - Application Interface (API):
ChallengeService
Application (Impl):
ChallengeServiceImpl
DRIVEN PORTS - Service Provider Interface (API):
ChallengeRepository
,TimeProvider
DRIVEN ADAPTERS - Infrastructure (Impl):
PostgresChallengeRepository
,SystemTimeProvider
3-Layer Architecture
We’re starting off with a 3-layer architecture. We’re building a “Code Typing“ system which has various coding challenges.
We have three layers:
Presentation: Representing presentation concerns (in this case, a REST API). For example, the
ChallengeController
. We take HTTP request body as input, pass the request to theChallengeService
(see below) and then return HTTP status code & HTTP response body.Business Logic: Modelling business logic concerns. For example, the
ChallengeService
contains business logic associated with challenges - e.g., to create a challenge, we analyze the incoming challenge textual content length as well as the current date to set the challenge difficulty level, and then call theChallengeRepository
(see below) to save the challenge.Infrastructure: Modelling I/O concerns, system time, external web calls. For example, the
ChallengeRepository
enables us to persist challenges and retrieve challenges from a database. We also make a direct call to the system clock (e.g.Clock.systemUTC.instant()
in Java,DateTime.Now
in .NET).
Problems?
The following are some problems faced with the above:
Business Logic is coupled to Infrastructure.
Firstly, the business logic is coupled to I/O concerns, for example, the database. So if we perform a large ORM upgrade (with breaking challenges), or choose a different ORM (or perhaps raw SQL), or switch to a different database provider (e.g. from Postgres to MongoDB), then it has ripple changes on both Repositories and Services.
Secondly, the business logic is coupled to sources of non-determinism, e.g., the System Clock. So this means we are unable to test various pathways in the business logic since we cannot control system time. (Or if we control the system time, we cannot run those tests in isolation from each other).
Presentation Logic is coupled to Business Logic.
Since the REST API controllers are directly coupled to business logic (the service implementation classes), it means we may find it cumbersome to test various REST API responses due to the effort in setting up state to get the business logic to return certain responses.
Furthermore, the REST API controllers are also transitively coupled to infrastructure too. For example, the REST API controller may return ORM entities, or due to dependencies on system time we might not be able to test certain REST API responses dependent on logic dependent on time.
Solutions?
Let’s apply Inversion of Control (Dependency Inversion):
Introduce an abstraction over the infrastructure, so that the business logic is not coupled to any sources of I/O or non-determinism. This means that that we can then fully unit-test our business logic.
Introduce an abstraction over the business logic (specifically, a facade), so that the presentation is not coupled to the business logic. This means we can test the various responses for our REST API by “mocking out“ the business logic facade.
Hexagonal Architecture
Let’s model our Application (Business Logic) in isolation from the external world (Presentation & Infrastructure).
APPLICATION
DRIVER PORTS - Application Interface (API) - representing an interface that will be exposed to the application’s client. For example, we’re declaring the
ChallengeService
interface which will be consumed by clients (e.g., the REST API ControllerChallengeController
). This means the client (ChallengeController class
) will NOT be able to directly access the business logic (ChallengeServiceImpl
class), but instead only call the Driver Ports (ChallengeService
interface).Application Implementation - we implement the Driver Ports, i.e., implementing the Application Interface (API). For example, we have the class
ChallengeServiceImpl
which implement theChallengeService
interface (Driver Port).DRIVEN PORTS - Service Provider Interface (SPI) - represents the needs of our application to perform its work. For example, the class
ChallengeServiceImpl
needs some persistence mechanism for storing challenges (expressed through the Driven Port -ChallengeRepository
interface) as well as access current time (expressed through the Driven Port -TimeProvider
interface). In this way, our application is declaring its needs through interfaces that need to be fulfilled by some service providers in the external world.
ADAPTERS
DRIVER ADAPTERS - presentation implementation, so that the external world can consume our application. Driver Adapters (e.g., REST API controllers) call the Driver Ports. For example, the
ChallengeController
(Driver Adapter) calls theChallengeService
(Driver Port).DRIVEN ADAPTERS - infrastructure implementation, providing access to databases, file systems, network, system time. For example, the
PostgresChallengeRepository
class (Driven Adapter) implements theChallengeRepository
interface (Driven Port); theSystemTimeProvider
class (Driven Adapter) implements theTimeProvider
interface (Driven Port).
Impact
The impact of Hexagonal Architecture is that we “isolate“ our Application from the external world, whereby the only connection between the Application and the external world is through the Ports exposed by the Application. Consequently, our Application is 100% unit-testable.
The external world (Driver Adapters) can call the Application via Driver Ports. Our Application can call the external world (Driven Adapters) via the Driven Ports.
Watch on YouTube
You can watch the Live Coding session Hexagonal Architecture - Episode 3 - Live Coding with Marcus Rådell and view the source code on GitHub:
Another comment, this time related with lingüistic domain.
When I speak in terms of "Driver" / "Driven" , I don't relate these terms with "Frontend" (Driver) and with "Persistence" (Driven); no, I speak in terms of "controllers", "databases" and so forth. So, when you create folders...do you create folders called "driver", "driven" or "controllers", "persistence"...?
Another deeper level than "driver" is "Controller" and much deeper than "controller" is "render" and "non-render" (it includes controllers which send json, xml, cvs, download pdfs and so on...).
How to stop of creating levels? xD
I consider "Presentation" as infrastructure, hence my three layers are: infrastructure, application and domain.
Inside infrastructure I add controllers, databases and so forth.