Implementing Hexagonal Architecture with DDD in TypeScript
Credit: Herberto Graca

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

  • Driving (primary) actors initiate interactions (e.g., a controller taking user input and passing it to the Application via a Port).
  • Driven (secondary) actors are triggered by the Application (e.g., a database Adapter called by the Application to fetch data).

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.

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:

  1. The controller (Driving Adapter) initiates interactions with the Driving Port.
  2. The Driving Port, implemented as an interface in the Application Layer (Hexagon).
  3. The Application Service, as the orchestrator, implements the Driving Port.
  4. The Driven Port is implemented by the Driven Adapter.

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 😇

Like
Reply

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

Like
Reply

To view or add a comment, sign in

More articles by Mobashir Hasan Haidery

Others also viewed

Explore content categories