Hexagonal Architecture in Java

Shaheen Zahedi
4 min readJun 6, 2021

1. Overview

In this article, we go through a practical example of Hexagonal Architecture.

First we introduce the shape of the architecture. Second we dig in to the pros and cons of this type of architecture in a real world software system. And finally we discuss a scenario where we implement all the points mentioned.

2. Introduction

Hexagonal architecture also known as “ports and adapters” or in some cases “boundaries, controllers, entities”; is a type of architecture where business logic is separated from the technical implementation which as uncle Bob mentions it’s a detail and should be separated from the core logic.

Let’s see:

According to the picture the core looks like a hexagon which is a representation of it’s name. Inside the core we have entities and use cases — notice all the arrows pointing towards the core and not the other way around — with a little bit of thinking about the picture we figure out why it’s called “Ports and Adapters”. As you see there are various adapters that provide core’s need of the outside world. However, there are two types of ports, input and output. Input on ports on the left side controls our application because they call the core and on the right side there are ports.

3. Strengths

Using this architecture promotes testability. As you see in the picture our domain logic or use cases is not dependent upon any code that needs outside environment, such as databases or network. Which is a great benefit to the process of having tests that are small, fast and independent of any infrastructural frameworks.

Another Benefit is that it abstracts the complexities of domain. When we separate the domain use cases from other layers, we basically prevent changes in domain logic from affecting the presentation layer, and it’s a great fit to an enterprise application, because the end-user communicates with the system acting as use cases rather than directly with domain entities.

It also makes it flexibe to apply changes on the adapters easily. For example for database, your business rules are not bounded to it. you can change your SQL database to something like Mongo or something else.

Major point in using this type of architecture is to use dependency injection, because it allows you to decouple domain logic from all those external infrastructure, and also reduces the number of reasons that we need to change domain logic which leads to better maintainability.

4. Weaknesses

We have to develop mechanisms to select adapters at runtime, which also decreases performance. another weakness is that, some quality attributes like reliability or security are decided by the adapters, sometimes you might end up using or selecting a third-party adapter that are not a good fit, so you need to select them with care.

Now we see some code demonstration:

5. Demonstration

Suppose we have an entity, named Person, this entity is in the core of our project. We want to demonstrate how to write a ‘create’ use case for it:

public class Person {
Long id;
String name;
String firstName;
Integer age;
//Getters and Setters
}

6. Use Cases

@FunctionalInterface
public interface CreatePersonUseCase {
Long createPerson(CreatePersonCommand command);
}
@Service
public class CreatePersonService implements CreatePersonUseCase {
private final CreatePersonPort createPersonPort;
@Autowired
public CreatePersonService(final CreatePersonPort createPersonPort) {
this.createPersonPort = createPersonPort;
}
@Override
public Long createPerson(final CreatePersonCommand command) {
final Person person = new Person(
null, command.getName(), command.getFirstName(), command.getAge());
return createPersonPort.createPerson(person);
}
}

And finally we have a port. As we described, any adapter that wants to manipulate data in the core has to use the port for that:

@FunctionalInterface
public interface CreatePersonPort {
Long createPerson(Person person);
}

7. Adapters

We demonstrate two adapters, that implements the same Port and finally we see that we can easily switch between adapters:

@Service
public class CreatePersonAdapter implements CreatePersonPort {
//Constructor injecting createPersonH2Repo
@Override
public Long createPerson(final Person person) {
final PersonEntity personEntity = new PersonEntity( person.getName(), person.getFirstName(), person.getAge());
return createPersonH2Repo.save(personEntity).getId();
}
}
@Service
public class CreatePersonAdapter implements CreatePersonPort {
//Constructor injecting createPersonNeo4JRepository
@Override
public Long createPerson(final Person person) {
final PersonEntity personEntity = new PersonEntity( person.getName(), person.getFirstName(), person.getAge());
return createPersonNeo4JRepository.save(personEntity).getId();
}
}

8. REST Controllers

public class CreatePersonController {
//Constructor injecting createPersonUseCase
@PostMapping("/create")
public ResponseEntity<Long> createPerson(@RequestBody final CreatePersonCommand command) {
return new ResponseEntity<>(createPersonUseCase.createPerson(command), CREATED);
}
}

9. Conclusion

In this article we learned about hexagonal architecture and it’s strengths and weaknesses. Then we saw a very brief example of how it looks in a real world scenario.

In conclusion this approach will make our software, more testable, flexible, maintainable. Also there are lot of other advantages like modifiability or we can talk about how it’s a good fit for domain-driven desgin.

--

--