This page looks best with JavaScript enabled

Hexagonal Architecture in Java

 ·   ·  ☕ 6 min read  ·  ✍️ Anirban

1. Overview

In this tutorial, we’ll take a look into the hexagonal architecture in Java. To illustrate this further, we’ll create a Spring Boot application.

2. Hexagonal Architecture

The hexagonal architecture describes a pattern for designing software applications around the domain logic. The hexagon describes the core of the application consisting of the domain object and the use cases of the application. The edges of the hexagon provide the inbound and outbound ports to the outside parts of the hexagon such as web interface, databases, etc.

So, in this kind of software architecture, all the dependencies between the components point towards the domain object. Therefore, the communication between the core application and the outside part is only possible using ports and adapters. In the following sections, we can have a deep dive into the different layers of hexagonal architecture.

3. Domain Object

The domain object is the core part of the application. It can have both state and behaviour. However, it doesn’t have any outward dependency. So any change in the other layers has no impact on the domain object.

The domain object changes only if there is a change in the business requirement. Hence, this is an example of the Single Responsibility Principle among the SOLID principles of software design.

First, let’s create a domain object Product that forms the core of the application. It contains product-related information and business validations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Product {

    private Integer productId;
    private String type;
    private String description;

    public Product() {
        super();
    }

    public Product(Integer productId, String type, String description) {
        this.productId = productId;
        this.type = type;
        this.description = description;
    }

    //getters
}

4. Ports

The ports are interfaces that allow inbound and outbound flow. Therefore, the core part of the application communicates with the outside part using the dedicated ports.

4.1. Inbound Port

The inbound port exposes the core application to the outside. It is an interface that can be called by the outside components. These outside components calling an inbound port are called primary or input adapters.

Let’s define a ProductService interface which is the inbound port:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface ProductService {

    List<Product> getProducts();

    Product getProductById(Integer productId);

    Product addProduct(Product product);

    Product removeProduct(Integer productId);
}

4.2. Outbound Port

The outbound port allows outside functionality to the core application. It is an interface that enables the use case of the core application to communicate with the outside such as database access. Hence, the outbound port is implemented by the outside components which are called secondary or output adapters.

Let’s define a ProductRepository interface which is an outbound port:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public interface ProductRepository {

    List<Product> getProducts();

    Product getProductById(Integer productId);

    Product addProduct(Product product);

    Product removeProduct(Integer productId);
}

5. Adapters

The adapters are the outside part of the hexagonal architecture. So, they interact with the core application only by using the inbound and outbound ports.

5.1. Primary Adapters

The Primary adapters are also known as input or driving adapters. Therefore, they drive the application by invoking the corresponding use case of the core application using the inbound ports. For example, primary adapters are REST APIs or web interfaces.

Let’s define a ProductController class as our primary adapter. In particular, it’s a REST controller that provides endpoints for creating and accessing products. Subsequently, it uses the inbound port service to interact with the core application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RestController
@RequestMapping("/api/v1/product")
public class ProductController {

    private ProductService productService;

    @Autowired
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public ResponseEntity<List<Product>> getProducts() {
        return new ResponseEntity<List<Product>>(productService.getProducts(), HttpStatus.OK);
    }

    @GetMapping("/{productId}")
    public ResponseEntity<Product> getProductById(@PathVariable Integer productId) {
        return new ResponseEntity<Product>(productService.getProductById(productId), HttpStatus.OK);
    }

    @PostMapping
    public ResponseEntity<Product> addProduct(@RequestBody Product product) {
        return new ResponseEntity<Product>(productService.addProduct(product), HttpStatus.OK);
    }

    @DeleteMapping("/{productId}")
    public ResponseEntity<Product> removeProduct(@PathVariable Integer productId) {
        return new ResponseEntity<Product>(productService.removeProduct(productId), HttpStatus.OK);
    }
}

5.2. Secondary Adapters

The Secondary adapters are also known as output or driven adapters. These are implementations of the outbound port interface. The use case of the core application invokes the secondary adapters using the output port. For instance, secondary adapters are connections to the database and external API calls.

Let’s define a ProductRepositoryImplementation class as our secondary adapter. In particular, this class implements the outbound port interface ProductRepository and allows the core application to access the database:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Repository
public class ProductRepositoryImplementation implements ProductRepository {

    private static final Map<Integer, Product> productMap = new HashMap<Integer, Product>(0);

    @Override
    public List<Product> getProducts() {
        return new ArrayList<Product>(productMap.values());
    }

    @Override
    public Product getProductById(Integer productId) {
        return productMap.get(productId);
    }

    @Override
    public Product addProduct(Product product) {
        productMap.put(product.getProductId(), product);
        return product;
    }

    @Override
    public Product removeProduct(Integer productId) {
        if(productMap.get(productId)!= null){
            Product product = productMap.get(productId);
            productMap.remove(productId);
            return product;
        } else
            return null;

    }
}

The adapters provide flexibility to the application without influencing the core application logic. If the application can be used by a new client in addition to the existing one, we can add the new client to the inbound port.

In addition, if the application requires a different database, we can add a new secondary adapter implementing the same outbound port.

6. Use cases of the core application

The use cases of the core application are the inside part of the hexagonal architecture. They are specific use case implementations of the inbound port. Hence, it contains all the use case-specific business rule validations and logic. The use case has no outside dependency similar to the domain objects.

Let’s define a ProductServiceImplementation class that provides the specific use case implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class ProductServiceImplementation implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public List<Product> getProducts() {
        return productRepository.getProducts();
    }

    @Override
    public Product getProductById(Integer productId) {
        return productRepository.getProductById(productId);
    }

    @Override
    public Product addProduct(Product product) {
        return productRepository.addProduct(product);
    }

    @Override
    public Product removeProduct(Integer productId) {
        return productRepository.removeProduct(productId);
    }
}

7. Conclusion

The hexagonal architecture offers several benefits compared to a layered architecture:

  • It simplifies the architecture design by separating the inside and outside parts of the application
  • The core business logic is isolated from any external dependencies which help to achieve a high degree of decoupling
  • The ports allow flexibility in connecting to new adapters in the form of new web clients or databases

The hexagonal architecture might be an overhead for designing simple CRUD applications. However, this architecture is useful when we are designing a domain-driven application.

The code for these examples is available over on Github.

Share on
Support the author with

Anirban Chatterjee
WRITTEN BY
Anirban
Software Engineer, Blogger