Overview

In this tutorial, we'll implement a simple Java application applying hexagonal architecture.

Hexagonal Architecture

Hexagonal architecture (a.k.a. Ports and Adapters Architecture) is a design pattern that isolates the core domain logic from all other application components.

As a result, the business core will communicate with other parts of the application through ports and adapters. This way, we can change the underlying technologies without having to change the application core.

Let's move on to the actual implementation of this architecture.

Implementation Using Java

We're going to build a small PhoneBook application with an add operation to show how to organize code around ports and adapters.

We can divide our application into three layers; domain (internal component), application (external component), and infrastructure (external component).

Domain Layer

The domain layer defines the inside of the application and provides ports to communicate with the application's use cases.

Firstly, we should create a PhoneBook class:

public class PhoneBook {

    private Long id;
    private List numbers;

    public PhoneBook(Long id, List numbers) {
        this.id = id;
        this.numbers = numbers;
    }

    public void addNumber(String number) {
        numbers.add(number);
    }

    // standard setters and getters
}

Anything related to our business logic will go through this class. Additionally, PhoneBook is responsible for adding a phone number.

Secondly, we should define the incoming ports. These are used by external components to interact with our application.

Let's create a port for the add use case:

public interface AddPhoneNumberPort {
    void add(Long id, String number);
}

Thirdly, we should define our outgoing ports. These are used by the domain to interact with the storage.

In this example, we can define two ports: the first one for Load and the second one for Save.

public interface LoadPhoneBookPort {
    Optional load(Long id);
}
public interface SavePhoneBookPort {
    void save(PhoneBook phoneBook);
}

Lastly, we should implement the PhoneBookAddService to tie all the pieces together.

Notice how the service implements the incoming port in which it uses the outgoing ports.

public class PhoneBookAddService implements AddPhoneNumberPort {

    private LoadPhoneBookPort loadPhoneBookPort;
    private SavePhoneBookPort savePhoneBookPort;

    // constructor

    @Override
    public void add(Long id, String number) {
        PhoneBook phoneBook = loadPhoneBookPort.load(id).orElse(new PhoneBook(id, new ArrayList<>()));

        phoneBook.addNumber(number);

        savePhoneBookPort.save(phoneBook);
    }
}

On the add method, it uses the LoadPhoneBookPort port to fetch the PhoneBook from the storage. Then, it performs the changes in the domain model. And finally, it saves those changes through the SavePhoneBookPort port.

Infrastructure Layer

In this section, we need to provide implementations for the defined ports to help retrieve and save the data. We call these adapters.

Firstly, we should define a repository class that will be used later by the adapters:

public class PhoneBookRepository {

    List phoneBooks = new ArrayList<>();

    public void save(PhoneBook phoneBook) {
        phoneBooks.add(phoneBook);
    }

    public Optional load(Long id) {
        return phoneBooks.stream().filter(phoneBook -> phoneBook.getId().equals(id)).findFirst();
    }
}

Secondly, we should implement the adapters and use the repository.

Briefly, they load and save PhoneBook by calling the load and save methods of the repository class.

public class LoadPhoneBookAdapter implements LoadPhoneBookPort {

    private PhoneBookRepository phoneBookRepository;

    // constructor

    @Override
    public Optional load(Long id) {
        return phoneBookRepository.load(id);
    }
}
public class SavePhoneBookAdapter implements SavePhoneBookPort {

    private PhoneBookRepository phoneBookRepository;

    // constructor

    @Override
    public void save(PhoneBook phoneBook) {
        phoneBookRepository.save(phoneBook);
    }
}

Application Layer

In this section, we'll implement the application layer. We'll use the adapters for the outside entities to interact with the domain.

Therefore, let's create a PhoneBookApplication:

public class PhoneBookApplication {
    private AddPhoneNumberPort addPhoneNumberService;

    private PhoneBookRepository phoneBookRepository;

    private SavePhoneBookPort savePhoneBookAdapter;
    private LoadPhoneBookPort loadPhoneBookAdapter;

    public static void main(String[] args) {
        new PhoneBookApplication().init();
    }

    public void init() {
        phoneBookRepository = new PhoneBookRepository();

        savePhoneBookAdapter = new SavePhoneBookAdapter(phoneBookRepository);
        loadPhoneBookAdapter = new LoadPhoneBookAdapter(phoneBookRepository);

        addPhoneNumberService = new PhoneBookAddService(loadPhoneBookAdapter, savePhoneBookAdapter);
        addPhoneNumberService.add(100L, "064502131");
    }
}

Conclusion

In this article, we've learned how to implement the hexagonal architecture in Java.

As always, the code for these examples is available over on GitHub.