Discover more from Optivem Journal
Hexagonal Architecture - Live Coding with Marcus Rådell
How do we transform a traditional 3-layer architecture into Hexagonal Architecture (Ports & Adapters)?
Traditional 3-Layer architecture:
Hexagonal Architecture (Ports & Adapters):
DRIVER ADAPTERS - Presentation:
DRIVER PORTS - Application Interface (API):
DRIVEN PORTS - Service Provider Interface (API):
DRIVEN ADAPTERS - Infrastructure (Impl):
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 the
ChallengeService(see below) and then return HTTP status code & HTTP response body.
Business Logic: Modelling business logic concerns. For example, the
ChallengeServicecontains 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 the
ChallengeRepository(see below) to save the challenge.
Infrastructure: Modelling I/O concerns, system time, external web calls. For example, the
ChallengeRepositoryenables us to persist challenges and retrieve challenges from a database. We also make a direct call to the system clock (e.g.
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.
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.
Let’s model our Application (Business Logic) in isolation from the external world (Presentation & Infrastructure).
DRIVER PORTS - Application Interface (API) - representing an interface that will be exposed to the application’s client. For example, we’re declaring the
ChallengeServiceinterface which will be consumed by clients (e.g., the REST API Controller
ChallengeController). This means the client (
ChallengeController class) will NOT be able to directly access the business logic (
ChallengeServiceImplclass), but instead only call the Driver Ports (
Application Implementation - we implement the Driver Ports, i.e., implementing the Application Interface (API). For example, we have the class
ChallengeServiceImplwhich implement the
ChallengeServiceinterface (Driver Port).
DRIVEN PORTS - Service Provider Interface (SPI) - represents the needs of our application to perform its work. For example, the class
ChallengeServiceImplneeds some persistence mechanism for storing challenges (expressed through the Driven Port -
ChallengeRepositoryinterface) as well as access current time (expressed through the Driven Port -
TimeProviderinterface). In this way, our application is declaring its needs through interfaces that need to be fulfilled by some service providers in the external world.
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 the
DRIVEN ADAPTERS - infrastructure implementation, providing access to databases, file systems, network, system time. For example, the
PostgresChallengeRepositoryclass (Driven Adapter) implements the
ChallengeRepositoryinterface (Driven Port); the
SystemTimeProviderclass (Driven Adapter) implements the
TimeProviderinterface (Driven Port).
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:
Thanks for reading Optivem Journal! Subscribe for free to receive new posts.