Hexagonal Architecture — Is It Worth the Investment?

Explore the Hexagonal architecture and evaluate the benefits and challenges of its adoption

--

There are many architecture styles, each comes with a specific solution to a given architectural design problem. Based on the project need and context, the project architect needs to carefully evaluate and estimate the cost of his/her architecture decisions. In this article, I try to expose, in as much detail as possible, the Hexagonal architecture style and evaluate the benefits and challenges of its adoption, as well as, to allow technical leaders to have a clear idea of the attributes of this architecture pattern.

Hexagonal architecture, also known as Ports and Adapters architecture, is a software architectural pattern that emphasizes the separation of concerns in software development. It is a way of organizing the codebase of a software system, making it more modular, maintainable, and testable.

The hexagonal architecture was first introduced by Alistair Cockburn in 2005 as an alternative to the traditional three-tier architecture. The hexagonal architecture is based on the idea of a hexagon, with the core business logic in the center and the adapters and ports around it. The core business logic represents the heart of the system, where the domain-specific rules and behaviors are implemented. It is where the business value of the software system resides.

The Hexagonal Architecture

Let’s take a closer look at each component:

1. Core Business Logic: The core business logic is the central part of the system where the domain-specific rules and behaviors are implemented. It is responsible for processing the data and performing the necessary computations to generate a response. It is the part of the system that provides value to the users.

2. Ports: The ports are the entry and exit points of the system. They define the interfaces through which external requests are received and responses are sent back. A port can be thought of as a contract that specifies what the system can do and what information it needs to operate.

3. Adapters: Adapters are the implementation of the ports. They are responsible for converting the external requests into a format that the core business logic can process and converting the responses from the core business logic into a format that can be sent back to the user. Adapters also handle communication with external systems such as databases, message queues, and APIs.

What is the motivation for this architectural style?

The hexagonal architecture allows for the separation of concerns between the core business logic and the implementation details. This makes it easier to test and maintain the system. Testing can be done on the core business logic in isolation from the adapters and ports, which makes it easier to write tests that are reliable and repeatable.

Replacing the adapters and ports is also easier because they are separated from the core business logic. This makes it possible to swap out an adapter or port without affecting the core business logic. This is especially important for systems that need to be highly adaptable and scalable.

What are the strengths and challenges of this architecture style?

Strengths:

  1. Modularity: Hexagonal architecture promotes separation of concerns in the architecture. It isolates business logic from dependencies. This allows developers to focus on specific components independently. It becomes easier to plug in new features or modify existing ones without affecting the entire codebase.
  2. Testability: this architecture allows unit testing to be more efficient, it focuses on testing feature in the core business logic. It promotes an isolation of testable components of the system. The adapters and ports can be tested using integration tests. This separation improves testability, enables the use of mocking frameworks, and helps achieve higher code coverage.
  3. Adaptability: Hexagonal architecture supports adaptability by making it easier to replace adapters and ports without impacting the core business logic. This flexibility allows the system to evolve and integrate with different external systems or technologies over time. It enables the development of highly scalable and extensible applications.
  4. Domain-driven design: Hexagonal architecture aligns well with the principles of domain-driven design (DDD). It provides a clear separation between the domain model and technical infrastructure, allowing developers to focus on capturing and implementing complex domain rules. This makes the codebase more expressive and easier to understand.
Unit test for business logic

Challenges:

  1. Increased complexity: Hexagonal architecture introduces additional layers of abstraction and indirection, which can increase complexity compared to simpler architectural patterns. It requires careful design and understanding of the system’s requirements to ensure a proper balance between flexibility and complexity. It also requires more initial development effort compared to traditional architectures.
  2. Learning curve: Hexagonal architecture may have a steeper learning curve for developers who are unfamiliar with the pattern. Understanding the concepts, designing the boundaries between the core business logic and adapters/ports, and managing dependencies require a solid grasp of architectural principles and design patterns.
  3. Performance considerations: The indirection introduced by hexagonal architecture can have a minor impact on performance compared to more streamlined architectures. The additional layers and abstraction may introduce a slight overhead in terms of processing time and memory consumption. However, in most cases, the benefits of modularity and maintainability outweigh this minor performance impact.

Use case and sample code:

Consider a simple e-commerce system where users can buy products online. The hexagonal architecture can be used to implement this system as follows:

1. Core Business Logic: The core business logic of the e-commerce system would handle the processing of orders, managing inventory, and handling payment transactions.

2. Ports: The e-commerce system would have two types of ports, one for receiving requests from users (inbound) and one for sending responses back to the user (outbound). The user request port would receive requests for placing an order, checking order status, and managing their account. The response port would send back the response to the user in a format that is understandable to them.

3. Adapters: Adapters would be responsible for handling communication with external systems such as databases, payment gateways, and shipping services.

Code sample:

  1. Inbound Port:

As described above, an inbound port represents an interface through which external requests are received by the system. It serves as the entry point for the system, allowing it to interact with external actors, such as users, other systems, or devices. The inbound port defines the contract for communication, specifying the expected input parameters, data format, and potentially any validation rules or error handling.

The order service inbound port would be an interface for the Hexagon:

public interface OrderServicePort {
void placeOrder(OrderRequest orderRequest);
}

The OrderServicePort is an inbound port that represents the capability of placing an order within the e-commerce system. The placeOrder method defines the contract for accepting an order request.

Order service adapter would implement the inbound port interface:

public class OrderServiceAdapter implements OrderServicePort {
private final OrderProcessor orderProcessor;

public OrderServiceAdapter(OrderProcessor orderProcessor) {
this.orderProcessor = orderProcessor;
}

@Override
public void placeOrder(OrderRequest orderRequest) {
// Perform any necessary validation or pre-processing of the order request
// before passing it to the core business logic
// ...

// Pass the order request to the core business logic for further processing
orderProcessor.processOrder(orderRequest);
}
}In this example, the OrderServiceAdapter class implements the OrderServicePort interface and delegates the actual order processing to the OrderProcessor component. The adapter is responsible for any necessary pre-processing or validation of the order request before passing it to the core business logic.

The core business logic, represented by the OrderProcessor component, resides within the system and performs the actual processing of the order. It is decoupled from the implementation details and external dependencies, allowing it to focus solely on the domain-specific rules and behaviors.

2. Outbound port:

As described above, an outbound port represents an interface through which the system communicates with external dependencies, such as databases, third-party APIs, message queues, or other systems. It serves as the exit point for the system, allowing it to send data or requests to external actors. The outbound port defines the contract for communication, specifying the expected output parameters, data format, and potentially any error handling or response handling.

The outbound port PaymentGatewayPort would be an interface for the Hexagon:

public interface PaymentGatewayPort {
boolean processPayment(PaymentRequest paymentRequest);
}

The PaymentGatewayPort is an outbound port that represents the capability of processing payments within the e-commerce system. The processPayment method defines the contract for sending a payment request to a payment gateway.

The PaymentRequest object passed as a parameter to the processPayment method encapsulates the necessary information for processing a payment, such as the payment amount, customer details, and payment method.

The implementation of the PaymentGatewayPort will be provided by an adapter, which is responsible for handling the communication between the system and the external payment gateway. The adapter will convert the outbound request into a format that the payment gateway can process.

Order service adapter would implement the outbound port interface:

public class PaymentGatewayAdapter implements PaymentGatewayPort {
private final PaymentGatewayClient paymentGatewayClient;

public PaymentGatewayAdapter(PaymentGatewayClient paymentGatewayClient) {
this.paymentGatewayClient = paymentGatewayClient;
}

@Override
public boolean processPayment(PaymentRequest paymentRequest) {
// Convert the payment request into a format expected by the payment gateway
// ...

// Send the payment request to the payment gateway
PaymentResponse paymentResponse = paymentGatewayClient.sendPayment(paymentRequest);

// Process the payment response and handle any errors or exceptions
// ...

// Return the payment status (success or failure)
return paymentResponse.isSuccess();
}
}

The PaymentGatewayAdapter class implements the PaymentGatewayPort interface and utilizes the PaymentGatewayClient to interact with the external payment gateway. The adapter converts the outbound request into a format expected by the payment gateway, sends the request, and processes the response.

The core business logic within the system can utilize the PaymentGatewayPort to initiate payment processing by invoking the processPayment method.

Towards Hexagonal Microservices?

Hexagonal architecture and microservices are two popular architectural patterns that can be combined to create scalable, resilient, maintainable, and loosely coupled systems.

Microservice architecture can definitely benefit the all the strength mentioned above of Hexagonal architecture in order to decrease the coupling between business logic components and technical dependencies. When applying hexagonal architecture principles to microservices, the focus is on encapsulating the core business logic within each microservice, while keeping the implementation details and external dependencies separate.

A third dimension/ingredient to Hexagonal Microservices can be Domain-Driven Design. We can design domains, subdomains and bounded context, then break it down into one or more microservices with leveraging Hexagonal principles to isolate the domains from external dependencies.

Conclusion

In conclusion, hexagonal architecture offers several advantages such as modularity, testability, adaptability, and alignment with domain-driven design principles. While these advantages are extremely important to a well architected system, it comes with an investment cost such as complexity, learning curve, etc.

If you are designing a system where clear separation of layers (for reasons such as evolvability, maintainability and testability) is structural then Hexagonal architecture is a good architecture pattern choice to adopt.

In general, in deciding on the architecture patten, one should contextualize the system and evaluate the long-term ROI (Return on Investment) of adopting such an architecture style. This will depend on how well the architecture is applied, the expertise of the development team, and the specific needs of the project.

--

--