Hexagonal Architecture: A Practical Guide
Created using DALL·E
In today’s fast-paced development environment, organising code effectively is critical for building scalable, maintainable, and testable applications. While many developers rely on layered architecture, this often leads to challenges with tight coupling, domain leakage, and difficulty in testing.
Hexagonal Architecture is a pattern that offers flexibility and a clean separation between business logic and technical details.
**_A little background:
_My journey with Hexagonal Architecture started in 2020, thanks to my colleague Alexey, who introduced me to the concept. Back then, we found only a few theoretical articles and struggled with vague examples, leading us to hours of experimentation. In this article, I want to demystify Hexagonal Architecture and provide a practical implementation guide using Java and Spring.
Why Move Away from Layered Architecture?
Layered architecture has long been the default approach for structuring applications, and it’s served us well. For many straightforward projects, it provides a familiar and logical way to organise code, separating concerns like controllers, services, and repositories. However, as applications grow and evolve, layered architecture can become a barrier rather than a benefit. Let’s look at some of the common limitations:
- Tight Coupling: Components within layered architectures often become interdependent, making it difficult to adapt or replace specific layers without impacting others. This tight coupling reduces flexibility when your application needs to evolve or integrate with new technologies.
- Testing Challenges: Business logic often spreads across multiple layers, complicating testing and leading to tests that rely on complex setups.
- Reduced Flexibility: Changing or updating the domain model, especially when tied to technology-specific code, can be costly. As technology stacks change, updating core domain logic intertwined with technical layers becomes cumbersome and error-prone.
When we start new services, we often set up a clean, organised package structure following layered architecture. But despite our best efforts, over time, this structure tends to degrade. The strict boundaries blur as we add new features, respond to changing requirements, or add integrations, leading to a messy, disorganised package structure.
Evolving to the huge shake on the right! How to handle it? Where to start?
Figure 1. Evolution from layered architecture to complicated package structure
(Created using DALL·E)
Layered architecture isn’t inherently bad or an anti-pattern. It’s a familiar default that often works well initially. But for complex, evolving applications, a change in approach can provide significant long-term benefits.
What is Hexagonal Architecture?
Hexagonal Architecture isn’t a radically new concept; rather, it’s a shift in perspective on how we structure code, putting the domain model, our mental representation of the real world, at the centre. By focusing on the domain, Hexagonal Architecture facilitates collaboration with both technical and non-technical stakeholders, making it easier to align development with business needs. At its core, Hexagonal Architecture is a domain-centric approach. The primary goal is to make the core domain of an application independent of technical details like APIs or databases. This independence is achieved by organising the application around three main components:
- Domain: The heart of the application, containing the core business logic and models. The domain is the only part of the code that truly understands the business and holds the rules that define it.
- Ports: Interfaces that enable communication between the domain and the outside world. Ports define expected interactions, allowing the domain to remain isolated from specific technical details.
- Adapters: Implementations of ports that connect the domain with external systems, such as databases, APIs, and user interfaces. Adapters handle the specifics of interacting with these systems, translating between external data formats and the domain’s own models.
This structure keeps the business logic isolated and independent from external systems. Adapters can be easily swapped out, making it easier to change or add new technologies without impacting the core domain.
Figure 2. Abstract hexagonal architecture
**_Never try to apply it for small projects with simple business logic!
_**It will increase code complexity and instead of storing your DTO to the database or passing to another service you will be forced to have at least 3 separate models and constant mapping from one to another.
Implementation: Designing a Bar System
Figure 3. Bar Order system example (Created using DALL·E)
To bring Hexagonal Architecture to life, let’s work through an example of software system for a bar. With following key features (or user stories):
- Take Order: Accept and manage orders from customers.
- Search Recipe: Look up recipes for various drinks.
- Mix Drinks: Prepare the ordered drinks based on recipes.
In a traditional layered architecture, the structure might look something like this on the left, while on the right, we can see how the same features would be structured using Hexagonal Architecture
Figure 4. Transition from layered package structure to hexagonal
Inbound Ports and Adapters: Controllers connect through “in ports” to the domain, handling requests or interactions initiated externally (e.g., HTTP requests). In this case, an order request would flow through an in port, used by OrderController and implemented by domain MixologyService.
Domain: This contains the core business logic. For example, a service like MixologyService would handle the logic for mixing drinks using the Cocktail model to represent the domain. It also interacts with external systems like recipes and orders.
Outbound Ports: “Out ports” define how the domain interacts with external systems. In this example RecipeCatalogOutPort would represent an interface to access recipe catalogue, which the domain service uses to look up drink recipes.
Figure 5. Mapping example to hexagonal architecture.
Let’s explore this structure in action with code snippets!
OrderController is an adapter that connects the outside world (HTTP requests) to the domain. It interacts with CocktailOrderInPort, an in port interface, without knowing the details of the domain logic.
class OrderController {
private CocktailOrderInPort cocktailOrderPort;
public CocktailDTO sendOrder( String orderRequest){
Cocktail cocktail = cocktailOrderPort.heyGiveMeDrink(orderRequest);
return CocktailDTO.from(cocktail); }
}
In this setup, MixologyService represents domain service, implementing the in port (CocktailOrderInPort). When processing the order, it uses RecipeCatalogOutPort to fetch recipes, keeping the domain logic isolated from technical details like database implementation. For simplicity, let’s assume the bar has unlimited ingredients and omit the BarStoragePort.
public class MixologyService implements CocktailOrderInPort {
private final RecipeCatalogOutPort recipeCatalogPort;
private final DomainEventObserverPort observer;
public Cocktail heyGiveMeDrink(String order) {
var maybeCocktail = recipeCatalogPort.find(order);
if (maybeCocktail.isEmpty()) {
observer.observe(MixologyEvent.failedToFindRecipe(order));
throw new RuntimeException("We don't have cocktail with " + order);
}
var cocktail = maybeCocktail.get();
observer.observe(MixologyEvent.cocktailMixed(cocktail.name()));
return cocktail;
}
}
Key Benefits of Hexagonal Architecture
Hexagonal architecture emphasises domain-centricity, where core logic is protected from external influence. This brings multiple advantages:
- Clear Separation of Concerns: The domain remains distinct from technology-specific components, simplifying domain-driven design.
- Increased Flexibility: Changing adapters, such as switching a database or external API, is straightforward.
- Enhanced Testability: Ports and adapters simplify unit testing by isolating domain logic from I/O concerns.
How to keep agreements and domain logic clean
Consistency is key to maintaining the integrity of Hexagonal Architecture. Without regular upkeep, even well-structured codebases can degrade over time, with domain rules, interface boundaries, and coding styles blurring or drifting. To counteract this, establish strong coding standards and regular code reviews to ensure adherence to architectural principles. However, keeping the domain isolated from technical details requires a bit more and this is where ArchUnit comes in.
ArchUnit is a Java library for checking architecture rules, allowing you to define constraints that reinforce the principles of Hexagonal Architecture. It helps to establish rules around package structure and control dependencies, ensuring that:
- Both Domain Services and Adapters should be inaccessible from other layers, keeping business logic independent of technical details.
- Ports act as the only interface between the Domain and Adapters, ensuring that all external communication passes through a well-defined boundary.
- Domain Models represent core business concepts and act as the “glue” between layers
Figure 6. Defined ArchUnit rules for hexagonal architecture
These rules prevent accidental leaks of technical specifics into the domain layer, preserving the clean boundaries that Hexagonal Architecture relies on.
Incorporating ArchUnit provides an automated way to enforce architectural boundaries in the code, reducing the need for manual oversight. By integrating these tests into your CI/CD pipeline, you maintain consistent application of architectural principles as the project grows.
Conclusion
A slight shift in how we structure and interpret real-world concepts in code enhances problem-solving clarity and strengthens collaboration with product stakeholders.
Hexagonal Architecture provides a flexible, maintainable, and testable approach to application design by clearly separating business logic from technical concerns. While it requires careful design and discipline, the long-term benefits, particularly for complex applications, make the initial investment worthwhile.
Combining Hexagonal Architecture with ArchUnit and dependency injection pattern (using Spring, in this case) further reinforces this approach. ArchUnit enforces architectural boundaries, ensuring consistency, while Spring’s dependency injection smoothly connects components, supporting a modular, scalable design that keeps business logic independent from technical details.
The code example used in the article available on Github