Hexagonal Architecture in Java

Sunaina Goyal
4 min readNov 29, 2021

--

1. Overview

In this tutorial, we will implement a Spring application to exhibit a practical yet simple example of the usage of the Hexagonal Architecture pattern. It is an illustration of the approach and the implementation of the pattern principles in Java.

2. Hexagonal Architecture

The hexagonal architecture provides a clean separation between the business domain (or the inside), and the technical code (or the outside) of the application.

The Application layer depends on the Domain layer to fulfill a request. The outside layers are dependent on the inside layers, but they can be ignorant of what the inner layers are doing — they just need to know the methods to call and data to pass. Implementation details are safely encapsulated.

Now, the Domain layer will likely need database access to create some domain entities. This means our application depends on data storage.

We can see here conceptually, our inner layers are depending on the outside. But ideally, the domain should be independent of the outside. So, we invert the control by using interfaces. These allow our layers to dictate how they are used. This is an Inversion of Control.

Let’s understand this by dividing our product-service application into three layers — application (outside), domain (inside), and infrastructure (outside) :

2. Domain

Let’s start with the domain, to build value-based software. Firstly, create a simple ProductDto class :

public class ProductDto {
private Long id;
private String type;
private String description;
// Getters and setters
}

3. Ports

3.1. Inbound Port

Now let’s define an interface ProductServicePort. It exposes the core application to the application layer :

public interface ProductServicePort {
List<ProductDto> getProducts();
ProductDto addProduct(ProductDto product);
//..
}

3.2. Outbound Port

Create another interface ProductPersistencePort. The implementation of the interface will be in the infrastructure layer :

public interface ProductPersistencePort {
List<ProductDto> getAllProducts();
ProductDto addProduct(ProductDto product);
//...
}

4. Adapters

Adapters are the outside part of the application. They interact with the domain by using only the inbound and outbound ports.

4.1. Primary Adapters

They drive the application using the inbound port. Examples of primary adapters could be REST APIs or an interface contract with your clients. Let’s define a ProductController that provides endpoints for creating and fetching the resources :

@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductServicePort productService;
@GetMapping
public ResponseEntity<List<ProductDto>> getProducts() {
return new ResponseEntity<List<ProductDto>>(productService.getProducts(), HttpStatus.OK);
}

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

//..
}

4.2. Secondary Adapters

Secondary adapters are an implementation of the outbound port. They offer the flexibility of changes in the data access layer (framework or database changes), without affecting the domain.

Create a Spring Data Jpa Repository ProductRepository in the infrastructure. This JPA repository saves the database entity Product to the database :

@Repository

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Now we have to transform the request object ProductDto to entity class Product and then save it to the database. For this, create a ProductJpaAdapter class, using the inversion of control :

@Service
public class ProductJpaAdapter implements ProductPersistencePort {

@Autowired
private ProductRepository productRepository;
@Override
public List<ProductDto> getAllProducts() {
List<Product> products = productRepository.findAll();
return ProductMapper.INSTANCE.fromProductList(products);
}

@Override
public ProductDto addProduct(ProductDto productDto) {
Product product = ProductMapper.INSTANCE.toProduct(productDto);
Product productSaved = productRepository.save(product);
return ProductMapper.INSTANCE.fromProduct(productSaved);
}
//..
}

5. Use case of the core application

They are specific use case implementations of the inbound port. Let’s define a ProductServiceImpl class in the domain :

class ProductServiceImpl implements ProductServicePort {
private final ProductPersistencePort productRepository;
ProductServiceImpl(ProductPersistencePort productRepository) {
this.productRepository = productRepository;
}
@Override
public List<ProductDto> getProducts() {
return productRepository.getAllProducts();
}
//...
}

6. Conclusion

In this article, we’ve learned to layer our classes/objects in such a way that the core logic is isolated from the external elements.

In our application above, we see that the REST endpoints internally use the inbound port to communicate with the domain layer. The domain uses the outbound port to communicate with the downstream system. It enables us to replace one adapter with another without any impact on the domain. For example, if you want to implement a new Jpa Provider, you just need to define a new repository with @Repository annotation, while removing the current ProductRepository. This will require no change in the ProductServiceImpl. The domain depends on nothing but itself:

--

--

Sunaina Goyal
Sunaina Goyal

Written by Sunaina Goyal

Developer, melomaniac, philospher, tea lover

Responses (1)