Implementing Hexagonal Architecture with DDD in TypeScript
In software development, the Hexagonal Architecture, also known as Ports & Adapters, offers a way to create applications that are adaptable, maintainable, and testable. By isolating the core business logic from external tools and delivery mechanisms, it allows for easy replacement and evolution of technology, aligning with principles from Domain-Driven Design (DDD). When I first learned about this pattern, I read a very helpful article by Herberto Graca, which you can find here.
The Traditional Layered Architecture
Before delving into the Hexagonal Architecture, let's briefly touch upon the traditional Layered Architecture. It aims to divide an application into different tiers, typically including the Presentation Layer, Logic Layer, and Data Layer. While this approach promotes separation of concerns, it lacks a natural mechanism to prevent business logic from leaking into other layers.
What is Hexagonal Architecture?
The Hexagonal Architecture, as envisioned by Alistair Cockburn, provides a way to build software systems with a central core, decoupled from its interactions with the outside world. It accomplishes this through the use of "ports" and "adapters." Let's delve into these concepts.
Why a Hexagon?
The hexagon visually represents the multitude of Port/Adapter combinations that an application can have. It distinguishes the "driving side" (initiating actors) from the "driven side" (actors responding to the application's requests).
Driving Side vs Driven Side
Dependency Inversion in the Hexagonal Architecture Context
The Dependency Inversion Principle is crucial in this architecture. On the driving side, the Adapter depends on the Port, while the Application Service implements the Port's interface. On the driven side, the Application Service depends on the Port, and the Adapter implements the Port's interface. This inversion of dependencies ensures that high-level and low-level modules adhere to abstraction and depend on it.
Ports
In the context of Hexagonal Architecture, a port is an interface that serves as an entry and exit point for the application. Ports define the interactions between your application's core business logic and the outside world. These ports are often represented as interfaces.
// UserSearchPort.ts
interface UserSearchPort {
searchUsers(query: string): Promise<User[]>;
}
// UserProfileUpdatePort.ts
interface UserProfileUpdatePort {
updateUserProfile(userId: string, data: UserProfile): Promise<void>;
}
Adapters
Adapters are classes that implement the ports. They act as intermediaries, translating the core application's needs into interactions with external services, libraries, or technologies.
// UserSearchAdapter.ts
class UserSearchAdapter implements UserSearchPort {
constructor(private searchEngine: SearchEngine) {}
async searchUsers(query: string): Promise<User[]> {
// Use the search engine to find users
const results = this.searchEngine.search(query);
return results.map((result) => User.fromSearchResult(result));
}
}
// UserProfileUpdateAdapter.ts
class UserProfileUpdateAdapter implements UserProfileUpdatePort {
constructor(private userRepository: UserRepository) {}
async updateUserProfile(userId: string, data: UserProfile): Promise<void> {
// Use the user repository to update the user's profile
await this.userRepository.update(userId, data);
}
}
Application
The Application, represented as a hexagon, serves as the core of the system. It houses Application Services orchestrating use cases and the Domain Model encapsulating business logic within Aggregates, Entities, and Value Objects. The Application communicates with external actors via Ports.
Recommended by LinkedIn
Alignment with DDD
Incorporating the Hexagonal Architecture within DDD yields compelling benefits. The Application (Hexagon) accommodates both the Application and Domain layers while keeping the User Interface and Infrastructure layers external. This alignment enforces strict separation, preventing the leakage of domain logic into application layers.
Dependency Injection Containers
In the following example, the Hexagonal Architecture consists of the central core (business logic) and the adapters. You can use Dependency Injection Containers (DIC) to manage the injection of specific adapters.
// DIC.ts
const container = new Container();
container.bind<UserSearchPort>('UserSearchPort').to(UserSearchAdapter);
container.bind<UserProfileUpdatePort>('UserProfileUpdatePort').to(UserProfileUpdateAdapter);
By configuring the DIC to inject different adapters, you can seamlessly switch between different external tools or technologies. This adaptability is crucial when, for example, transitioning from one search engine (e.g., SOLR) to another (e.g., Elasticsearch) as your business needs evolve.
Benefits of Hexagonal Architecture
The Hexagonal Architecture, combined with DDD principles, offers several advantages:
Implementation and Technology Isolation
With ports and adapters, the core business logic is shielded from the specifics of external tools and technologies. As your technology stack evolves, you can simply create new adapters that conform to the existing ports.
Delivery Mechanisms Isolation
Hexagonal Architecture allows you to have multiple delivery mechanisms (e.g., web GUI, CLI, web API) that interact with the core logic through adapters. Changing or adding delivery mechanisms doesn't impact the core business logic.
Testing
Testing becomes more straightforward. You can isolate the core business logic from external dependencies by using mock or stub implementations of the ports during testing.
Structuring Your App
An illustrative example of building a simplified application using Ports and Adapters reveals a clear structure:
Conclusion
Hexagonal Architecture, when integrated with Domain-Driven Design (DDD) principles, offers a robust approach to building adaptable, maintainable, and testable software. By using ports and adapters, you can achieve a clean separation of concerns, making your applications more resilient to technological changes and adaptable to evolving business requirements.
With Hexagonal Architecture, your software system is designed to withstand the test of time, ensuring that your core business logic remains at the center, impervious to the changes in technology and delivery mechanisms that the future may bring.
Although understanding this instantly is not easy, but thanks from your article, finally I can find article's Herberto Graca about DDD in TS. You saved me 😇
I suggest to not include "port" and "adapter" part in the class name. The port should be general but adapter should include the use case or particular implementation e.g InMemoryUserService
👍
Good read! 👍🏻👍🏻