objectiveview.online
objectiveview.online

ObjectiveView

for the serious software developer - since 1996

Overview of Hexagonal ArchitectureValentina Cupać

pdf-download epub-download

Hexagonal Architecture (aka Ports and Adapters) is an architectural pattern which enables us build applications which can be developed, executed and tested without the UI and the database.Valentina Cupać explains...

Introduction

A Hexagonal Architecture facilitates the deployment of applications without their UI or database - enabling them to be run in isolation from the external technological environment. The essence of Hexagonal Architecture is the separation of the system core from the external world. This solves a key problem - architectural testability. Hexagonal Architecture enables us to build testable applications through Dependency Inversion.

In this article I review the progression of architecture from Traditional Layered Architecture towards Hexagonal Architecture and illustrate \ the implementation of Hexagonal Architecture through code samples in Java.

Traditional Layered Architecture

In the traditional layered architecture, the database is at the center of the system, the system cannot work without the database. Therefore, the sequence in which developers need to build the system is as follows:

figure 1 - a traditional layered architecture

The only way we can execute (and test) the application is with the database and with the UI. The application is dependent on technological concerns. This causes two problems:

To illustrate the first problem - the existing application may have a GUI, but due to client requirements there needs to be a REST API so that other systems can connect to our application. In Traditional Layered Architecture may need to rewrite a significant part of our application to be able to support two presentation layers.

To illustrate the second problem - the development of the business logic may be blocked by waiting on the database development. Furthermore, in the case that we need to switch to a different database technology, or significantly change the database schema, or migrate the ORM, then that has a cascading impact on the entire application.

Lastly, we cannot test the application behavior in isolation, because we require the UI and the database to be up-and-running in order for us to execute tests. This results in slow-running and fragile tests, making test automation an expensive and ineffective endeavour.

Hexagonal Architecture

figure 2 - as figure 1 with dependencies to infrastructure inverted - see dashed line

In Hexagonal Architecture, we apply the Dependency Inversion Principle (DIP - as in SOLID) in order to isolate the application from external technological concerns, so that we can execute (and test) the application in isolation from the UI and the database, network, and any other technical architectural concerns. We are testing the application behavior in isolation without any technology.

Inverting the dependencies within the Traditional Layered Architecture, we get the architecture shown in figure 2.

More formally, the underlying goal of Hexagonal Architecture is to “Create your application to work without either a UI or a database so you can run automated regression-tests against the application, work when the database becomes unavailable, and link applications together without any user involvement” and the intent of Hexagonal Architecture is to “Allow an application to equally be Server-side by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases” (Alistair Cockburn, https://alistair.cockburn.us/hexagonal-architecture/).

This enables us to plug-and-play the User-side Adapters (i.e. the application can be equally executed by some UI, REST API, Tests) and the Server-side Adapters (i.e. the application can be connected to any database or third party services). Consequently, we can swap out technological implementations. Hexagonal Architecture also served as a foundation for subsequent architectures such as Onion Architecture and Clean Architecture - both carry the fundamental concept that the application core is separated from the external world.

Let's take a look at the structure of a hexagonal archticture:

figure 3 - a hexagonal architecture

figure 4 - hexagonal architecture in UML to be more precise

See figure 4 for the same architecture represented in UML.

Implementing Hexagonal Architecture

We'll now take a look at how to implement a hexagonal architecture. To do this we'll go through a number of steps:

Test User-side Adapters

Firstly, we want to specify the application behavior in an executable way. In specifying behavior, we define the User-side Ports (corresponding to use cases), and we define the Server-side Ports (service provider interfaces - such as repository interfaces to abstract away the database). Test doubles are used as adapters for Server-side Ports. Let’s start with Test User-side Adapters. The following is an example of testing that given some product and some discount rate, that the product details we retrieve contains the correct discounted price. The test is targeting the SuperMarketApp - the application interface, which exposes User-side Ports (interfaces) - in this case the Product Management interface, which exposes related use cases, in this case the viewProductDetails use case.

This is really important to note, that the test is aware only of the hexagon ports and is not aware of the internal implementation. In this case, we have no knowledge about the implementation of the viewProductDetails use case - for example we don’t know if it’s implemented in OOP or FP way, we don’t know whether the implementation is one class or multiple classes. Furthermore, it should be note that the application has some dependencies on the external technological world - it needs some persistence mechanism for the products and the discount rates. We don’t want neither our test nor the source code to be coupled to the real database implementation, so instead we work with test doubles which are imitating the database. In the example below, we’ve used fakes as test doubles (FakeProductRepository and FakeDiscountRateRepository).

public class ViewProductDetailsTest {
	private FakeProductRepository productRepository;
	private FakeDiscountRateRepository discountRateRepository;
	private SupermarketApp supermarketApp;

	@BeforeEach
	protected void init() {
		productRepository = new FakeProductRepository();
		discountRateRepository = new FakeDiscountRateRepository();
		supermarketApp = new SupermarketAppImpl(
			productRepository, 
			discountRateRepository
		);
	}

	@Test
	void should_view_product_details_given_valid_sku() {
		var sku = "ABC";
		var price = 100.0;
		var discountRate = 0.05;
		var expectedDiscountPrice = 95.00;

		givenThatRepositoryContainsProduct(sku, price);
		givenThatRepositoryHasDiscountRate(discountRate);

		var expectedProductDetails = 
			new ProductDetails(sku, price, expectedDiscountPrice);
		var productDetails = 
			supermarketApp.getProductManagement()
			.viewProductDetails(sku);
		assertThat(productDetails).isEqualTo(expectedProductDetails);
	}

	private void givenThatRepositoryContainsProduct(
		String sku, double price
	) {
		var product = new Product(sku, price);
		productRepository.addProduct(product);
	}

	private void givenThatRepositoryHasDiscountRate(double discountRate) {
		discountRateRepository.setDiscountRate(discountRate);
	}
}

User-side and Server-side Ports

Through the test, we’ve modelled the hexagon ports (but not the internal hexagon implementation). Let’s see the hexagon ports - both the User-side Ports and the Server-side Ports. Our Application interface exposes the User-side Ports - Product Management and Discount Management ports:

public interface SupermarketApp {
	ProductManagement getProductManagement();
	DiscountManagement getDiscountManagement();
}

The following is an example of a User-side Port:

public interface ProductManagement {
	void addProduct(String sku, double price);
	ProductDetails viewProductDetails(String sku);
}

The following is an exmaple of a Server-side Port:

public interface ProductRepository {
	Optional < Product > getProduct(String sku);
	void addProduct(Product product);
}

Application Implementation

We implement our application - the internals of the hexagon. When implementing the application, we take the Server-side Ports (ProductRepository interface, DiscountRateRepository) and pass them to the implementations of our ports (ProductManagementImpl, DiscountManagementImpl).

public class SupermarketAppImpl implements SupermarketApp {
	private final ProductManagement productManagement;
	private final DiscountManagement discountManagement;
	
	public SupermarketAppImpl(
		ProductRepository productRepository, 
		DiscountRateRepository discountRateRepository
	) {
		this.productManagement = new ProductManagementImpl(
			productRepository, discountRateRepository
		);
		this.discountManagement = new DiscountManagementImpl(
			discountRateRepository
		);
	}

	public ProductManagement getProductManagement() {
		return productManagement;
	}

	public DiscountManagement getDiscountManagement() {
		return discountManagement;
	}
}

Let’s look at the implementation of a User-side port:

public class ProductManagementImpl implements ProductManagement {
	private ProductRepository productRepository;
	private DiscountRateRepository discountRateRepository;

	public ProductManagementImpl(
		ProductRepository productRepository, 
		DiscountRateRepository discountRateRepository
	) {
		this.productRepository = productRepository;
		this.discountRateRepository = discountRateRepository;
	}

	@Override
	public void addProduct(String sku, double price) {
		var product = new Product(sku, price);
		productRepository.addProduct(product);
	}

	@Override
	public ProductDetails viewProductDetails(String sku) {
		var product = productRepository.getProduct(sku).get();
		var discountRate = discountRateRepository.getDiscountRate();

		var regularPrice = product.getPrice();
		var multiplier = 1 - discountRate;
		var discountedPrice = regularPrice * multiplier;

		return new ProductDetails(sku, regularPrice, discountedPrice);
	}
}

Mock Server-side Adapters

For purposes of the unit test - which we use to test the application in isolation from external technological concerns, we need to implement Mock Server-side Adapters - though the more generic term is “test doubles”. The example below illustrates the implementation of Fakes.

public class FakeProductRepository implements ProductRepository {
	private HashMap<String, Product> products;

	public FakeProductRepository() {
		this.products = new HashMap<>();
	}

	@Override
	public void addProduct(Product product) {
		var sku = product.getSku();
		products.put(sku, product);
	}
	
	@Override
	public Optional < Product > getProduct(String sku) {
		var product = products.get(sku);
		return Optional.of(product);
	}
}

Real Adapters

Finally, we move onto implementing the -real- adapters.

Conclusion

In this article, we have seen that Hexagonal Architecture enables us to build applications in isolation from the external world through the Dependency Inversion principle. The key benefits provided by Hexagonal Architecture are that it enables us to test the application in isolation from technological concerns, as well as enabling us to achieve a plug-and-play mechanism whereby we can swap our external technologies without affecting the application.

References

  1. The source code examples are based on the Supermarket Pricing Kata (Java) See https://github.com/valentinacupac/supermarket-pricing-kata-java
  2. https://youtu.be/WAoqGzVDHc0 (by the author of this article)
  3. https://alistair.cockburn.us/hexagonal-architecture/
  4. https://jmgarridopaz.github.io/content/hexagonalarchitecture.html
  5. https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749