Hexagonal Architecture with TDD
The art of a clean, testable and easily maintainable architecture
Sponsor of the week: Multiplayer
Multiplayer auto-documents your system, from the high-level logical architecture down to the individual components, APIs, dependencies, and environments.
Perfect for teams who want to speed up their workflows and consolidate their technical assets.
Thank you to our sponsors who keep this newsletter free to the reader! If you want to sponsor us, click here.
Motivation
What is the goal of software architecture? According to Robert C. Martin:
A good architecture allows the software to be changed during its lifetime with as little, constant effort as possible - The Clean Architecture book
Onion, Clean, or Hexagonal architecture—I'm sure you've already heard of these. But what do they have in common?
They are all domain-centric architectures that follow the principle of dependency inversion. The domain is the core part of the app, while every dependency is directed towards the domain. Each architecture provides an opinionated way to structure your code, offering separation of concerns, loose coupling and better testability.
Today let’s talk about my favorite one Hexagonal Architecture, how you can architect your app with it, and how it can help with your Test-Driven Development workflow.
Hexagonal Architecture
Hexagonal architecture, also known as the Ports & Adapters pattern, solves the problem of separating our application from its dependencies. This architecture treats the application as a central core surrounded by a hexagon, where each side represents a port interfacing with external systems/services through adapters.
There are two types of ports and adapters – primary ones that control the application and secondary ones that are controlled by the application.
Two types of ports:
Primary port - providing entry points to our applications
Secondary port - providing an interface to access the external world
Two types of adapters:
Primary adapter - everything that wants to access our app via primary ports such REST API, MVC Controllers or a SOAP service
Secondary adapter - implementations of the secondary ports such as DB repositories or clients for 3rd party services
That’s it. Our application exposes ports, while adapters ensure connection from the outside world. Now let’s see how to apply hexagonal architecture on a real-life example application.
Real-life Application
Let's consider a Warehousing Management System that enables warehousing and logistics of electronic products. We have the following high-level architecture diagram.
As you can see, our app serves multiple clients and integrates various APIs, data components, and domain-specific business logic.
And now let’s see how we can structure our app using hexagonal architecture:
Solution with Ports & Adapters
The goal is to centralize our domain logic and completely isolate it from external components. By looking at the high-level architecture design, we will have two primary adapters that will access our app:
WarehousingAPI: used for our internal UI
OrderingService: used to enable a CLI tool for resellers
For each, we need corresponding ports on the edge of the hexagon. Ports are defined by interfaces. Think of them like regular coding interfaces, but it’s also common to provide ports via Command and Query objects of the CQRS pattern.
As for the secondary side, we need to define two adapters for the external systems:
ImagePersister: to store images of warehouse products in AWS S3 buckets
ProductRepository: to store our products in WarehousingDB (PostgreSQL)
For each, we need ports again, so we will create interfaces to abstract away external systems. Here is what our app would look like with Hexagonal Architecture:
Testability and TDD
Why is this so useful? Next to putting the focus on the most important part of your app - the business logic-, it also makes it easier to test it, especially when you use Test-Driven Development
Hexagonal architecture helps testing in 2 ways:
You can test all components in complete isolation using test doubles. Fakes are ideal for simulating persistence dependencies, while mocks are useful when you need to verify interactions with external systems
The driver ports clearly define the public APIs you can write your tests for. It also comes with the advantage that your tests won’t be coupled to any implementation details. Whether you use Transaction Script, Domain-Driven Design, or some other design approach inside the Application Core, it doesn't matter for the tests. It leads to a robust test suite that aids refactoring
Behaviour-focused tests and refactoring are a core part of TDD, hence hexagonal architecture will greatly support your TDD flow.
Conclusion
Hexagonal Architecture centralizes the domain and separates the application from its dependencies. It focuses on domain-centric designs, featuring dependency inversion with the core domain at the center, surrounded by ports and adapters for interfacing with external systems.
Hexagonal Architecture supports testability and Test-Driven Development. As a bonus, it also creates a clear, standardized structure everyone can follow, leading to sustainable software development.
How Can I Help You?
There are two ways I could help you:
Transform Your Craft With TDD: Join 200+ engineers and master Test-Driven Development. You’ll not just learn TDD; you’ll master the best practices to produce quality software via real-world projects.
Reach 22,000+ developers and decision-makers by sponsoring this newsletter
Hexagonal Architecture is crucial in enabling unit testability. We can unit test the Hexagon and write narrow integration tests for the Adapters.
Good code should be easy to change.
What this really means is that the code should be well-organized, easy to understand, easy to modify, and easy to test.
Thanks for the article, Daniel!