This is the second article of a 4 5 part series explaining Hexagonal Architecture and how to implement such an architecture using Spring Boot and TDD.

Part 1 - Introduction to Hexagonal Architecture and Key Concepts

Part 2 - Coding demo project and implementation of the API adaptor

Part 3 - Implementation of Domain Services

Part 4 - Implementation of MongoDB repository adaptor

Part 5 - Implementation of REST adaptor to an external data source

Update: The corresponding video is live on YouTube now

Spring Framework

Using the Spring Framework and its projects does not guarantee the hexagonal architecture or any good architecture. However, it is designed with the modularisation and dependency support consistent with hexagonal architecture.

Among other factors, its Dependency Injection and Inversion of Control patterns directly support the ports and adaptor architecture, and its integration capabilities accelerate adaptor implementation.

Demo Application: Stock Position and Market Value Service

I’ll demonstrate the development os an application how to implement a hexagonal architecture compliant application using Spring Boot. We want to implement a service that returns how many shares a user has (stock position) and the current market value. This requires integration to both a data repository and a online source of stock prices.

The code can be found in this Github repository. First let’s look at the pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>hexa-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hexa-demo</name>
<description>Hexagonal Architecture Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
view raw pom.xml hosted with ❤ by GitHub

In this article, I’ll describe the stages of coding and show the final code. There will be a screencast video to go through the step-by-step description of TDD and Hexagonal Architecture considerations in the process.

REST API Adapter

We’ll use TDD (Test Driven Development) for this project, so let’s start with a test for the REST API.

What should the API adapter test cover? Let’s list out the concerns of a API adapter.

  • It should present an endpoint (e.g. “GET /stock-position”) with optional selection predicates (e.g. “accept: application/json”)
  • It should check for authorisation when required
  • It should validate and convert the parameters in the request (which can come in path variables, query params, request body, header values, cookies, authentication, etc) into domain models and types where necessary
  • It should call one or more domain service with the converted parameters
  • It should convert the returned results from domain models into API DTOs (Data Transfer Object)
  • It should return an appropriate HTTP status

REST API Adapter

In other words, the concerns of the API adapter includes everything between a request and the domain service for that request, but does not include any domain logic at all.

The fully fleshed out API test is:

package com.example.hexademo.api;
// imports not shown
@WebFluxTest
public class GetStockPositionAndMarketValueApiTest {
@Autowired
private WebTestClient client;
// Domain Service
@MockBean
private GetStockPositionService getStockPositionService;
@MockBean
private GetStockMarketValueService getStockMarketValueService;
@Test
@WithMockUser("peterpan")
void get() {
// arrange
String symbol = "aapl";
String user = "peterpan";
StockPosition fakeStockPosition = fakeStockPosition(user, symbol);
when(getStockPositionService.get(user, symbol)).thenReturn(Mono.just(fakeStockPosition));
BigDecimal fakeMarketPrice = fakeAmount();
when(getStockMarketValueService.get(symbol, fakeStockPosition.getQuantity())).thenReturn(Mono.just(fakeMarketPrice));
// act
makeGetRequest(symbol)
// assert
.expectStatus().isOk()
.expectBody(GetStockPositionAndMarketValueApiResponseDto.class)
.value(dto -> assertAll(
() -> assertThat(dto.getSymbol()).isEqualTo(symbol),
() -> assertThat(dto.getQuantity().doubleValue()).isCloseTo(fakeStockPosition.getQuantity()
.doubleValue(), Offset.offset(0.01)),
() -> assertThat(dto.getCurrencyCode()).isEqualTo(fakeStockPosition.getCurrencyCode()),
() -> assertThat(dto.getCost().doubleValue()).isCloseTo(fakeStockPosition.getCost()
.doubleValue(), Offset.offset(0.0001)),
() -> assertThat(dto.getMarketValue()
.doubleValue()).isCloseTo(fakeMarketPrice.doubleValue(), Offset.offset(0.0001))
));
}
@Test
@WithMockUser("peterpan")
void emptyPosition() {
String symbol = "appl";
when(getStockPositionService.get("peterpan", symbol)).thenReturn(Mono.empty());
when(getStockMarketValueService.get(eq(symbol), any()))
.thenReturn(Mono.just(fakeAmount()));
makeGetRequest(symbol)
.expectStatus().isOk()
.expectBody(Void.class);
}
@Test
@WithAnonymousUser
void anonymousGet() {
makeGetRequest("aapl")
.expectStatus().isForbidden();
}
@Test
void unauthenticatedGet() {
makeGetRequest("aapl")
.expectStatus().isUnauthorized();
}
private WebTestClient.ResponseSpec makeGetRequest(String symbol) {
return client.get().uri("/stock-position-market-value/" + symbol)
.accept(MediaType.APPLICATION_JSON)
.exchange();
}
}

And the corresponding REST Controller

package com.example.hexademo.api;
// imports not shown
@RestController
public class StockPositionsController {
private final GetStockPositionService getStockPositionService;
private final GetStockMarketValueService getStockMarketValueService;
public StockPositionsController(
GetStockPositionService getStockPositionService,
GetStockMarketValueService getStockMarketValueService) {
this.getStockPositionService = getStockPositionService;
this.getStockMarketValueService = getStockMarketValueService;
}
@GetMapping("/stock-position-market-value/{symbol}")
Mono<GetStockPositionAndMarketValueApiResponseDto> getPositionAndMarketValue(
@AuthenticationPrincipal Mono<Principal> principalMono,
@PathVariable String symbol
) {
return principalMono.flatMap(principal -> getStockPositionService.get(principal.getName(), symbol))
.zipWhen(stockPosition -> getStockMarketValueService.get(symbol, stockPosition.getQuantity()),
(stockPosition, marketValue) -> new GetStockPositionAndMarketValueApiResponseDto(symbol,
stockPosition.getQuantity(), stockPosition.getCurrencyCode(), stockPosition.getCost(), marketValue));
}
}

Noted that the DTO for the response body, GetStockPositionAndMarketValueApiResponseDto has a long specific name. This is intentional as this DTO is specifically for this API endpoint. Unless clearly intended and specified in the API specs, using the same DTO for different responses is unnecessary coupling.

Back to Part 1

Continue to Part 3