Software architecture represents the structure and behavior of a system most commonly built in multiple components. Depending on the software one would like to create, there are many different principles called architectural patterns to follow to ensure having firm foundations for building high-quality, easily adaptable systems. One of them is hexagonal architecture, developed by Alistair Cockburn.
Brief overview of hexagonal architecture
The major idea of hexagonal architecture was to isolate core business logic from external sources (e.g., database, web framework, etc.) so that it is not dependent on any of them. That is why it is alternatively called Ports and Adapters architecture.
Ports are interfaces of application that allow applications to interact with external systems and vice versa. Therefore, there are two types of ports: driver ports and driven ports. Driver ports are components that allow outside sources to interact with applications, mainly through API. On the other hand, driven ports are interfaces specified by the application so that it is able to interact with external sources.
Adapters are specific implementations of these ports. Accordingly, there are also driver and driven adapters. Driver adapters are the ones that use API to communicate with the core, and driven adapters are implementations that enable applications to communicate with external systems.
When it comes to layers, there are three of them in hexagonal architecture: domain, application, and infrastructure.
The domain layer is the central layer of an application. It is situated at the core of the hexagon and is unaware/independent of outside layers. Domain layer is completely focused on business logic, constraints, and related services.
The application layer is placed in between the domain and infrastructure layer. Translation of communication between external sources and the domain is what the application layer is responsible for. That is why controllers are usually found in this layer.
All systems not part of the application’s core, such as database and external libraries, appear outside of the hexagon. The infrastructure layer contains the essentials required for interaction with those outside sources. Additionally, application configurations are part of this layer.
Basic flow through an example
The hexagonal architecture was briefly explained in the previous section so that the basic flow presented below could be understood better.
An example is going to be a simple application that enables creating orders and adding products to them.
As mentioned, the domain layer is in the center of the hexagon. Having that in mind, implementation usually starts with it.
public class Product { private String id; private String name; private String description; public Product(String id, String name, String description) { this.id = id; this.name = name; this.description = description; } // getters and setters }
public class Order { private String id; private List<Product> products; private double price; public Order(String id, List<Product> products, double price) { this.id = id; this.products = products; this.price = price; } public Order(String id, Product product) { this.id = id; this.products = List.of(product); this.price = product.getPrice(); } public void addProduct(Product product) { products.add(product); } // getters and setters }
After creating domain classes, a repository interface may be created, which acts as a driven port.
public interface OrderRepository { Order getOrderById(String id); void saveOrder(Order order); }
Later, a service adapter that implements a service port could be built. It enables orders to be saved – which permits interaction with external sources, meaning these are driver adapter and port.
public class OrderServiceImpl implements OrderService { private OrderRepository orderRepository; public OrderServiceImpl(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Override public Order getOrder(String id) { return orderRepository.getOrderById(id); } @Override public void addProduct(String orderId, Product product) { Order order = getOrder(orderId); order.addProduct(product); } }
public interface OrderService { Order getOrder(String id); void addProduct(String orderId, Product product); }
Enabling users to interact with an application is achieved through a controller. That is the reason why controllers are associated with driver adapters.
@RestController public class OrderController { private OrderService orderService; @PostMapping("/orders") public void createOrder(@RequestBody OrderUpsertRequest request) { orderService.createOrder(request.getProduct()); } @PostMapping("/products/{id}") public void addProduct(@PathVariable String id, @RequestBody ProductUpsertRequest request) { orderService.addProduct(id, request.getProduct()); } }
Lastly, code responsible for interaction with a database may be implemented.
public class DatabaseOrderRepository implements OrderRepository { private final OrderRepository orderRepository; public DatabaseOrderRepository(OrderRepository orderRepository) { this.orderRepository = orderRepository; } @Override public Order getOrderById(String id) { return orderRepository.getOrderById(id); } @Override public void saveOrder(Order order) { orderRepository.saveOrder(order); } }
Conclusion
Dividing this simple application into code snippets and explaining each one individually helps to visualize hexagonal architecture. It is a type of architecture usually used in way more complex systems where it could be the most benefited from.
After understanding theoretical basics and an uncomplicated example, it should not be impossible to implement it properly in the appropriate case.
“Hexagonal architecture through a basic flow” Tech Bite was brought to you by Nejra Sadžak, Junior Software Engineer at Atlantbh.
Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.