Hexaview Technologies

Domain-Driven Design and the Hexagonal Architecture

Share on linkedin
LinkedIn
Share on twitter
Twitter
Share on facebook
Facebook

Domain-Driven Design

Discovery

It’s rare to see a project achieve its ultimate form, if one even exists, following a single cycle of planning and execution in modern software development. Market demands evolve, assumptions are tested, and perceptions shift. Discovery begins before the first line of code is written and continues throughout the project’s life cycle.
However, only feedback can lead to a change in perception. This could be due to end-user-driven changes in requirements, a product owner’s a more complex design, or a developer discovering that code they tried to implement is logically inconsistent. Discovery is thus a collaborative process that anyone who has used an agile development methodology will be familiar with.
A mental model that gathers the business rules and relationships to be expressed in code and rectified by a continual refactoring process is a result of this ever-evolving understanding.

Why Domain-Driven Design?

Domain-Driven Design is a software development approach that focuses on the model of the business problem being solved and allows both technical and non-technical people to participate in its discovery through the use of a common language known as the “Ubiquitous Language.” Once a model has been established in this language, it may be converted into readable code using a common set of building blocks. Because the model can develop in response to product iteration, this methodology lends itself particularly well to Agile Development methodologies.


The following sections will walk you through the steps of discovering a model and implementing a solution in code. Wherever possible, alternative methodologies will be used to explain the motivations for Domain-Driven Design.

Hexagonal Architecture

We should aim for loose coupling and strong cohesiveness in the codebase when developing new software and refactoring legacy code; software components should have little direct knowledge of the inner workings of other components, and each module should have a clear and single purpose.

Domain-Driven Design results in a layer of software that is already detached from the rest of the system. Because business rules are separate from the user interface, persistence details, and third-party services, they can be simply (and quickly) tested.
Most modern web frameworks for PHP are based around the Model-View-Controller pattern. Each layer’s function could be summarised as follows:

  • Model: Coordinates domain operations, including querying/persistence concerns.
  • View: Provides facilities for user input and renders information about the model.
  • Controller: Accepts commands from the view, triggers behavior in the model and queries the output model.

Each of these layers is reliant on the others in some way, and each serves multiple purposes. Each action that is performed by the application is bound to the controller coordinating it and the model is in charge of both business logic and persistence.

By creating a dependency rule, we can decouple these layers:

Enter Hexagonal Architecture is also known as the Ports and Adapters.

The outer layers may only depend on inner layers. The inner layers should not be reliant on the outer layers. Each layer is defined as follows.

  • Infrastructure
  • Application Layer
  • Domain Layer

Infrastructure Layer

The Infrastructure Layer contains code that interfaces with application infrastructure – controllers, UI, persistence, and gateways to external systems. Many of the objects found in this layer will be provided by a web framework or persistence library. Concretions of domain repositories are placed in this layer while the actual interfaces are defined in the domain layer.

Application Layer

The Application Layer provides an API for all functionality provided by the application. It accepts commands from the client (whether web, API, or CLI) and translates these into values understood by the domain layer. As an example, a RegisterUser service would accept a Data Transfer Object containing a new user’s credentials and delegate responsibility for the creation of a user to the domain layer.

Domain Layer

The Domain Layer contains any core domain logic. It deals entirely with domain concepts and possesses no knowledge of the outer layers.

Ports and Adapters

We’ve talked about the domain model and the application services that surround and interact with it up to this point. These application services, on the other hand, are entirely useless if clients can’t use them, which is where ports and adapters come in.

A port is a special type of interface between a machine and the outside world that was created for a specific purpose or protocol. As a result, we can define the port as a technology-independent application programming interface (API) that was created for a specific form of interaction with the application (hence the word “protocol”). It’s entirely up to you how you define this protocol, which is part of what makes this method so appealing. Here are some examples of the various ports you might have:

  • A port used by your application to access a database
  • A port used by your application to send out messages such as e-mails or text messages
  • A port used by human users to access your application
  • A port used by other systems to access your application
  • A port used by a particular user group to access your application
  • A port exposing a particular use case
  • A port designed for polling clients
  • A port designed for subscribing clients
  • A port designed for synchronous communication
  • A port designed for asynchronous communication
  • A port designed for a particular type of device

Let’s have a look at the hexagonal architecture diagram again:

 

A port is represented on each side of the inner hexagonal. This is why this architecture is frequently shown in this way: you have six sides out of the box, each of which can be used for a different port, and plenty of room to plug in as many adapters as you need.

What is an Adapter?

As I already stated, ports are technology agnostic. Even so, you use technology to communicate with the system: a web browser, a mobile device, a dedicated hardware device, a desktop client, and so on. Adapters are very in this situation. An adapter enables interaction through a specific port and with a certain technology. Consider the following scenario:

  • A REST adapter allows REST clients to interact with the system through some port
  • A RabbitMQ adapter allows RabbitMQ clients to interact with the system through some port
  • An SQL adapter allows the system to interact with a database through some port
  • An adapter allows human users to interact with the system through some port

Numerous adapters for a single port are possible, as well as a single adapter for multiple ports. You can add as many adapters as you want or need to the system without affecting the other adapters, ports, or domain model.

Advantages:

  • Shared (ubiquitous) language for everyone: Domain-Driven Design focuses on establishing a common language among domain specialists, programmers, and consumers. This common language is utilized throughout the development process, including while programmers are writing code. Having a shared language eliminates uncertain terminology from the equation and ensures that all parties are on the same page as the domain evolves.
  • Focuses on defining the domain and the problem is solved: We’re making software for people to use. Domain-Driven Design is concerned with establishing the domain and the challenges that must be solved in real-world scenarios. Instead of planning out how you’ll arrange your database tables or API endpoints in advance, you’ll think in terms of use cases or user stories.
  • Great for documentation: Because you’re writing code in a common (ubiquitous) language, it’s simple for people to look at it and figure out what’s going on. When developing the code, for example, verbs that you’d use in real life to express a use case would be employed.
  • Clear separation of layers: I particularly like how the Domain, Application, and Infrastructure layers are separated in Hexagonal Architecture. If addressed right, this creates some clear boundaries around how everything interacts and helps programmers avoid adding complexity to their code.
  • Great for flexibility and refactoring: The domain of software is frequently complex, and it evolves with time. Technical debt can build up quickly if you don’t take the right approach to code design. I’ve always admired how simple it is to refactor Domain-Driven Design code using Hexagonal Architecture. When it comes to continuous improvement, the separation of layers and the high-quality object-oriented design make it straightforward.
  • Feels like more code than needed for simple CRUD: One disadvantage is that there appears to be a lot of code for simple CRUD operations. At Tithe.ly, we’ve helped to tackle this with simple code generation technology that creates the necessary layers and unit tests. This helps us to quickly produce simple CRUD code so we can focus on the big issues.
  • Cost efficiency: Domain-Driven Design makes development a breeze, but it comes at a cost in terms of code efficiency. It’s difficult to quantify this, but in my opinion, I’d rather pay a little more on server costs to compensate for the lack of efficiency than have a codebase that is difficult to maintain and improve on.

Hopefully, this gives you an idea of why I’ve enjoyed working with Domain-Driven Design and hexagonal architecture. No approach is perfect, but I’ve always found it important to plan to make software that can be maintained for decades to come.

Bilal Ansari

Bilal Ansari

Bilal Ansari is the author of this article. He is a Member of the Technical Staff at Hexaview since Jan'20. He is highly skilled in Java, Spring Boot, Microservices, Kafka, and S3.