Write More Maintainable Software With a Hexagonal Architecture

Adopting the hexagonal architecture pattern produces software that is more maintainable. It enables you to respond to changes with less fuss than many other architectural patterns. In this article I’ll explain why and offer my thoughts on this pattern.

What is a Hexagonal Architecture?

Alistair Cockburn first coined the term Hexagonal Architecture on his blog in 2005. Also known as the Ports and Adapters pattern, it is a layered architecture. It is a way of separating the domain concerns while making unit tests easier to write and changes simpler to accommodate.

It’s cousin is the popular Model-View-Controller (MVC) pattern that separates the presentation, business, data and persistence layers. In contrast, a Hexagonal Architecture has the business domain objects and logic at its center. Surrounding it are ports (and adapters of those ports) where actions come in (like an HTTP POST), and where actions go out (like a database update).

Hexagonal Architecture
Hexagonal Architecture

In the above diagram, “Application (core)” is where your domain objects and business services reside. The MVC equivalents are Value Objects and Business Services. The black edges are the ports, which are interface classes in Java. This pattern forces a focus on the business domain. In fact, it leads you toward a Domain-Driven Design (DDD) where the design of the software (classes, objects, methods, etc.) use the same structure and terminology as the business domain. By closely linking the software product to the business this way, the development team has a better shared understanding with the business. After all, we should all be speaking the same language.

Surrounding the Application are the adapters. In Java, the adapters are concrete classes that implement the ports (interfaces).

By convention and to make it easier to understand, the adapters on the left are the Driving adapters; think of a controller class in a web application. The adapters on the right are the Driven adapters; think of a DAO or JPA class.

The Driving adapters have a reference to an application service in the form of an interface. Your favourite Dependency Injection (DI) framework injects the implementation of this service into the controller.

In turn, an application service has a reference to an outbound port (Driving port) an an interface class. Your DI framework sticks the implementation of the outbound port (a persistence adapter) into the application service.

What’s Good About It?

The strength of the Hexagonal Architecture is how it makes it more difficult to bleed across concerns. We’ve all seen instances where an MVC app begins its life with all the best of intentions, keeping presentation logic separate from business logic separate from persistence logic. All of these have clearly defined interfaces. Yet without careful attention, business logic gradually creeps into the web controllers, and presentation logic creeps into the data layer.

Where this logic creep causes problems is when you need to make a change to the software. A change to the data layers creates unnecessary impacts to (say) the presentation layer because someone decided to bypass the service layer. As a result, the magnitude of the change is bigger than it needs to be. This results in extra cost and effort, and leads to a software product that is unnecessarily brittle.

Another strength of this style is modularity. Put simply, it pushes you to adopt the principles of Single Responsibility, Interface Segregation and Dependency Injection. Therefore it makes your code easier to understand, easier to maintain, and easier to test with automated unit tests.

So far I’ve been using web controllers as implementations the Driving ports and databases as the implementation of Driven ports. These are not the only ways to implement the ports. You can use JMS messaging, another web service, a file repository, etc. as adapters. The beauty of this your application (services and domain objects) could care less about what’s on the other side of the ports. All your services do is talk to the ports.

What Are The Downsides?

You end up with more abstractions that you may have with an MVC style. Each of the ports needs an implementation. In addition, you need to convert the domain objects to and from persistence classes and web request and response objects. So you end up with more code than you might with other styles.

It takes some time and research to figure out how to adopt a Hexagonal Architecture to your specific context. To help you along, have a look at my examples here. Also, Tom Hombergs gave a presentation at Spring I/O in 2019 that does a great job explaining this pattern.

What Does It Look Like?

These examples come from QBD API, a web service I wrote for interacting with QuickBooks Desktop.

Most of us are familiar with the package structure of a conventional MVC layered architecture:

MVC architecture

Here is the same web service structured using a hexagonal architecture:

The same web service structured using a Hexagonal Architecture

Notice the package names ending in .port.in and .port.out. These are your Driving and Driven Adapters respectively. The .service package has the concrete implementations of your Driving ports (.port.in). All these are part of the .application package, which is the hexagon in the image above.

The .domain package has your business domain objects. They are not coupled at all to a data persistence or web framework. Rather, they are simply Plain Old Java Objects (POJOs).

Notice the class naming convention in the port.in package. By ending each of these interface class names with “UseCase”, you get a very succinct answer to the question “What does this service do?”

Show Me Some Code Examples!

Let’s begin in the middle – the Application. SearchForCustomerService looks like this:

package ca.airspeed.qbdapi.application.service;

import java.util.List;

import javax.inject.Singleton;

import ca.airspeed.qbdapi.application.port.in.SearchForCustomerUseCase;
import ca.airspeed.qbdapi.application.port.out.SearchForCustomerPort;
import ca.airspeed.qbdapi.domain.Customer;
import io.micronaut.core.annotation.Introspected;

@Singleton
@Introspected
public class SearchForCustomerService implements SearchForCustomerUseCase {

    private SearchForCustomerPort customerPort;

    public SearchForCustomerService(SearchForCustomerPort customerPort) {
        super();
        this.customerPort = customerPort;
    }

    @Override
    public List<Customer> findByFullName(String fullName) {
        return customerPort.findByFullName(fullName);
    }

}

This Application service extends an interface that a Driven Adapter uses. Furthermore, it with some implementation of the SearchForCustomerPort interface. It could care less how the implementation goes about its business, as long as it returns a List of Customer POJOs in response to a String argument.

A CustomerPersistenceAdapter implements the SearchForCustomerPort interface:

package ca.airspeed.qbdapi.adapter.out.persistence;

import java.util.List;

import javax.inject.Singleton;

import ca.airspeed.qbdapi.application.port.out.RetrieveCustomerPort;
import ca.airspeed.qbdapi.application.port.out.SearchForCustomerPort;
import ca.airspeed.qbdapi.domain.Customer;
import lombok.RequiredArgsConstructor;

@Singleton
@RequiredArgsConstructor
public class CustomerPersistenceAdapter implements RetrieveCustomerPort, SearchForCustomerPort {
    private final CustomerJpaRepository repo;
    private final CustomerMapper mapper;

    @Override
    public Customer findByCustomerId(String id) {
        return mapper.mapToDomainEntity(repo.findById(id));
    }

    @Override
    public List<Customer> findByFullName(String fullName) {
        List<CustomerJpaEntity> resultSet = repo.findByFullNameStartsWith(fullName);
        return mapper.mapToDomainList(resultSet);
    }

}

Notice this bad boy lives in the adapter package, specifically the out.persistence package. It’s a Driven adapter that is to the right of the Application hexagon in the diagram above. One of its collaborators is a JPA repository that reads from the database. In addition, it has a simple mapping class that converts (in our example) a Customer JPA Entity into a Customer domain object.

Notice this class is the first one that has any sort of reference to a specific persistence implementation. The Application service we talked about earlier talks to some implementation of the SearchForCustomerPort interface. What they have in common is the Customer domain object POJO.

Moving on to the Driving side, the adapter CustomerController looks like this:

package ca.airspeed.qbdapi.adapter.in.web;

import static io.micronaut.http.hateoas.Link.SELF;
import static java.lang.String.format;

import java.util.ArrayList;
import java.util.List;

import ca.airspeed.qbdapi.adapter.in.web.resource.CustomerResource;
import ca.airspeed.qbdapi.adapter.in.web.resource.SearchForCustomerResponseResource;
import ca.airspeed.qbdapi.application.port.in.RetrieveCustomerUseCase;
import ca.airspeed.qbdapi.application.port.in.SearchForCustomerUseCase;
import ca.airspeed.qbdapi.domain.Customer;
import io.micronaut.context.annotation.Value;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.security.annotation.Secured;
import lombok.extern.slf4j.Slf4j;

@Controller("/customers")
@Slf4j
public class CustomerController {

    private SearchForCustomerUseCase searchForCustomer;
    private RetrieveCustomerUseCase retrieveCustomer;

    @Value("${micronaut.server.contextPath}")
    private String serverContextPath;

    public CustomerController(SearchForCustomerUseCase searchForCustomer, RetrieveCustomerUseCase retrieveCustomer) {
        super();
        this.searchForCustomer = searchForCustomer;
        this.retrieveCustomer = retrieveCustomer;
    }

    @Secured("isAnonymous()")
    @Get("/search/fullNameStartingWith")
    @ExecuteOn(TaskExecutors.IO)
    public List<SearchForCustomerResponseResource> searchByFullNameStartingWith(@QueryValue String fullName) {
        List<Customer> data = searchForCustomer.findByFullName(fullName);
        List<SearchForCustomerResponseResource> results = new ArrayList<>();
        if (data == null) {
            return results;
        }
        for (Customer customer : data) {
            SearchForCustomerResponseResource result = SearchForCustomerResponseResource.builder()
                    .id(customer.getId())
                    .name(customer.getName())
                    .fullName(customer.getFullName())
                    .build();
            result.link(SELF, format("%s/customers/%s", serverContextPath, customer.getId()));
            results.add(result);
        }
        return results;
    }
    
    @Secured("isAnonymous()")
    @Get("/{customerId}")
    @ExecuteOn(TaskExecutors.IO)
    public CustomerResource findOneCustomer(String customerId) {
        log.info("Received a request for findOneCustomer().");
        Customer customer = retrieveCustomer.retrieveCustomer(customerId);
        if (customer == null) {
            return null;
        }
        else {
            CustomerResource result = new CustomerResource();
            result.link(SELF, format("%s/customers/%s", serverContextPath, customer.getId()));
            result.setId(customer.getId());
            result.setFullName(customer.getFullName());
            result.setName(customer.getName());
            return result;
        }
    }
}

Its two collaborators are the xxxUseCase interface classes (ports) we talked about. A Driving adapter has a reference to an Application Service. Similarly, an Application Service has a reference to the Driven adapter.

Should I Adopt It?

Well, like the answer to most any other question on software architecture and design, it depends. We’ve already talked about the trade-offs. If you decide a layered architecture is most appropriate for your situation, and if you have at least a few business rules to implement, then you’ll really see the benefits of a Hexagonal Architecture. On the other hand, if your application is mostly a CRUD application with little in the way of business rules, then an MVC pattern is probably better; Hexagonal Architecture would be overkill with the many abstractions you need to deal with.

The example code I provide is from QBD API, a mostly CRUD web service. Yes, Hexagonal Architecture is overkill in this case; I used it here for pedantic reasons – to learn.

In my experience, getting my head wrapped around this pattern was a tough slog. I had been using MVC for so many years it took some effort to rewire my brain. But I soon discovered a feeling of freedom with the Hexagonal Architecture. I actually found it to be liberating. I could focus on implementing business logic in the application (the center of the hexagon) without worrying about how I would implement the persistence. Just code the port (the interface class) and implement it later. Similarly, I didn’t need to think much about how the API would look to consumers until later. Same for the Driven adapters. After all, to quote Grzegorz ZiemoĊ„ski, “it’s just ports and adapters baby!”

In Summary

A Hexagonal Architecture is a layered architecture. Consider it when you might use an MVC pattern. It’s shines in situations where you have at least a few business rules to implement. On the other hand, you end up with more code and mappers owing to the abstrations you need to deal with.