Hexagonal Architecture in Java
A practical example of Hexagonal Architecture in Java
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 the hexagonal architecture.
3. Domain Object
The domain object is the core part of the application. It can have both state and behavior. 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
which forms the core of the application. It contains product-related information and business validations:
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 and setters
}
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 an interfaceProductService
which is the inbound port:
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.
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:
@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<>(productService.getProducts(), HttpStatus.OK);
}
@GetMapping("/{productId}")
public ResponseEntity<Product> getProductById(@PathVariable Integer productId) {
return new ResponseEntity<>(productService.getProductById(productId), HttpStatus.OK);
}
@PostMapping
public ResponseEntity<Product> addProduct(@RequestBody Product product) {
return new ResponseEntity<>(productService.addProduct(product), HttpStatus.CREATED);
}
@DeleteMapping("/{productId}")
public ResponseEntity<Product> removeProduct(@PathVariable Integer productId) {
return new ResponseEntity<>(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 classProductRepositoryImplementation
as our secondary adapter. In particular, this class implements the outbound port interface ProductRepository
and allows the core application to access the database:
@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 uses case has no outside dependency similar to the domain objects.
Let’s define a classProductServiceImplementation
which provides the specific use case implementation:
@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.
Originally published on Anirban’s Tech Blog