Hexagonal architecture in Rust
Tutorial index
- Chapter 1: Hexagonal architecture in Rust
- Chapter 2: Hexagonal architecture in Rust: the domain
- Chapter 3: Hexagonal architecture in Rust: the use cases
- Chapter 4: Hexagonal architecture in Rust: driving adapters
- Chapter 5: Hexagonal architecture in Rust: driven adapters
- Chapter 6: Hexagonal Architecture in Rust: Driven Adapter Switching — CQRS
- Chapter 7: Hexagonal Architecture in Rust: Driving Adapter Switching — GraphQL
Introduction
In this tutorial we will implement a Rust application using DDD and we will organize layers leveraging Hexagonal Architecture.
In order to reach a reliable code we will proceed with the implementation using TDD.
We will implement the CRUD required to supply a simple service of sandwich recipe management. This will also give us the opportunity to illustrate basic REST principles.
After this implementation, we’ll go through 2 simple examples to show how hexagonal architecture can isolate our domain logic from the external world.
In the end, we will containerize our application and we will enrich our project with a micro-service architecture deployed on Kubernetes.
Prerequisites
- basic Rust knowledge
- basic REST knowledge
- basic TDD knowledge
Technologies
Language: Rust
Web framework: Actix
Architectural style: REST
Persistence layer: MongoDB
Hexagonal architecture
Hexagonal architecture is an architectural pattern that aims to create loosely coupled application components.
The basic principle is the separation of the domain application logic from the external world and the creation of adapter components to connect them together.
As you can grasp, hexagonal architecture and DDD (domain-driven design) work very well together because of the central role played by domain logic.
Principles
Despite what the name may suggest, hexagonal architecture can be broken down into 4 kinds of components:
- domain
- use cases
- ports
- adapters
Domain
The domain layer comprises all those objects modelling the application domain and containing the application logic.
To better adhere to the principles of DDD, we should design these objects by trying to trace their names and interactions to their real domain counterparts. This lower abstraction leads to a better understanding of the domain and better synergy with the domain experts who usually provide the application requirements.
The domain is completely agnostic of external behaviours and doesn’t have any outward dependency. This helps us to guarantee the Single responsibility principle (the S in SOLID) ensuring that we can change the business logic only to implement change requirements and not to align to changes external to the system.
Use cases
Use cases are a further abstraction over the classic service layer and often they are designed separately to be then incorporated into the same implementation object.
They represent particular use cases to manage in the application flow (e.g. a search over some data, a money transfer, a recipe registration, etc.), they contain coordination logic to apply to the domain objects and they are useful to separate functionalities in small and well-defined pieces of code.
Furthermore, they act as an interface between the external world and the domain logic to guarantee, again, the independent evolution of the domain layer.
Ports
Ports are those components responsible for communicating with the external world, so with everything that lives outside of the hexagon.
A port can be a simple rule defining a contract between the outside world and our application.
Ports are divided into 2 groups:
- input ports
- output ports
Input ports, generally represented on the left of the hexagon, interact with driving adapters. They manage the requests incoming from the external world (any client querying our application) and generally are called by an adapter.
Output ports, generally represented on the right of the hexagon, interact with driven adapters, so-called because they are driven by our core. They ask for something from an external component (e.g. a database, external API, etc.) and generally are implemented by an adapter.
Adapters
Adapters are responsible for adapting external requests to the format expected (for input ones) or expressed (for output ones) by our ports.
An adapter can be anything (a REST controller, a web interface, a command line application, a persistence manager, etc.) that is able to communicate with a port or respect its contract.
It is easy to understand that for each port we can have multiple adapters. This really looks like the `trait` concept in Rust, so we will leverage it in this tutorial.
Adapters are divided into 2 groups:
- driving adapters
- driven adapters
Driving adapters drive the execution of our core functionalities by requesting something to an input port.
Driven adapters are queried by our ports in order to communicate with an external service.
Pros and Cons
Let’s have a look at the positive and negative implications of this architecture.
Pros
Domain isolation
By encapsulating the domain logic within the hexagon, we can ensure that the core of our logic is independent of the rest of the application. If at some point we need to change something outside the hexagon, we can safely apply our changes without worrying about the domain.
Flexibility
The hexagonal architecture enhances the flexibility of an application. The core of this feature lies within ports and adapters: by defining interaction contracts between the outside and inside of the hexagon we can switch between different adapter implementations without affecting the rest of the application.
Focus on the business logic
By isolating our domain logic, we can focus on the core business logic paying the most of our attention to it, deferring the remaining decisions at the end, when they are required.
Ease of development
By defining contracts between the components, it becomes easier to divide the application into parts that can be developed simultaneously by different developer teams.
Testability
By separating the application components, it becomes easier to test them. This is achieved with different techniques, but the most typical is often the use of doubles (as we will see in the next chapters).
Cons
Complexity
The pros listed above come at the cost of increased complexity. The whole system increases the number of its components in order to specify interaction contracts and this can easily lead to bigger software.
High number of data models
To separate each part of the hexagon, we increase the number of data models to communicate with each other and this can lead to confusion.
Conclusion
In this article, we have introduced the main principles of hexagonal architecture.
In the next chapter, we will begin creating a web application starting with its domain logic.