This is the fourth 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

Part 2 of the video is live now:

REST Adapter for Stock Market Price

To complete the service, the last interface to implement is GetStockMarketPricePort for retrieving the market price of the stock. We will use the free service form AlphaVantage for this.

alphavantage

There are several providers with different service levels and pricing that you could close from, and you can choose any and implement the corresponding adapter, without changing any other parts of the source code. Herein lies the power of the hexagonal architecture to modularise the code.

We start by choosing a suitable API endpoint to provide the required data, in this case since we only need the last closing price, we can use the TIME_SERIES_DAILY endpoint.

We’ll need a JSON mapper for the data returned by this endpoint, so let’s download a sample and place it into the test resource test/respurces/alphavantage-samples/time-series-daily.json.

{
"Meta Data": {
"1. Information": "Daily Prices (open, high, low, close) and Volumes",
"2. Symbol": "IBM",
"3. Last Refreshed": "2020-04-21",
"4. Output Size": "Compact",
"5. Time Zone": "US/Eastern"
},
"Time Series (Daily)": {
"2020-04-21": {
"1. open": "114.0000",
"2. high": "117.1450",
"3. low": "112.0600",
"4. close": "116.7600",
"5. volume": "14341304"
},
"2020-04-20": {
"1. open": "119.1500",
"2. high": "122.8635",
"3. low": "118.1400",
"4. close": "120.4100",
"5. volume": "7965530"
},
"2020-04-17": {
"1. open": "119.3000",
"2. high": "120.3900",
"3. low": "117.9200",
"4. close": "120.1200",
"5. volume": "4964018"
},
// truncated
}
}

We then create a test AlphaVantageTimeSeriesDailyJsonTest to make sure the mapping classes are correct.

package com.example.hexademo.alphavantage;
// imports not shown
class AlphaVantageTimeSeriesDailyJsonTest {
@SneakyThrows
@Test
void matchesJson() {
ObjectMapper mapper = new ObjectMapper();
File file = new ClassPathResource("alphavantage-samples/time-series-daily.json").getFile();
AlphaVantageTimeSeriesDailyJson json = mapper.readValue(file, AlphaVantageTimeSeriesDailyJson.class);
assertThat(json).isNotNull();
}
}

The resulting classes for the JSON mapping are

package com.example.hexademo.alphavantage;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AlphaVantageTimeSeriesDailyJson {
@JsonProperty("Meta Data")
private AlphaVantageTimeSeriesDailyJsonMetaData metaData;
@JsonProperty("Time Series (Daily)")
private Map<String, AlphaVantageTimeSeriesDailyJsonDaily> daily;
}
//
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AlphaVantageTimeSeriesDailyJsonDaily {
@JsonProperty("1. open")
private String openingPrice;
@JsonProperty("2. high")
private String highPrice;
@JsonProperty("3. low")
private String lowPrice;
@JsonProperty("4. close")
private String closingPrice;
@JsonProperty("5. volume")
private String volume;
}
//
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AlphaVantageTimeSeriesDailyJsonDaily {
@JsonProperty("1. open")
private String openingPrice;
@JsonProperty("2. high")
private String highPrice;
@JsonProperty("3. low")
private String lowPrice;
@JsonProperty("4. close")
private String closingPrice;
@JsonProperty("5. volume")
private String volume;
}

With that out of the way, we can now implement the request to AlphaVantage. Again, let’s start with the test. Like the repository test, this test is implementation independent. It should specify the expected behaviour of the interface GetStockMarketPricePort regardless of what the implementation is.

package com.example.hexademo.domain.service;
// imports not shown
@SpringBootTest
class GetStockMarketPricePortIntegrationTest {
@Autowired private GetStockMarketPricePort subject;
@Test
void get() {
String symbol = DomainModelFaker.fakeStockSymbol();
Mono<BigDecimal> result = subject.get(symbol)
.log();
StepVerifier.create(result)
.assertNext(item ->
assertThat(item).isGreaterThanOrEqualTo(BigDecimal.ZERO)
)
.verifyComplete();
}
}

The implementation is quite straight forward using this JSON mapper, and we’ll elaborate on one detail.

To use the service, we need an API key (sometimes called authentication token) from AlphaVantage. We want to keep this API key a secret so not should not be in the source code. To get around this, we’ll set the environment variable ALPHAVANTAGE_API_KEY to the value of this key.

We then use @Value("${alphavantage.api.key}") to retrieve this key in the code. Note that Spring automatically converts the environment variable ALPHAVANTAGE_API_KEY to match the alphavantage.api.key property.

package com.example.hexademo.alphavantage;
// imports not shown
@Service
@Slf4j
public class AlphaVantageGetStockMarketPrice implements GetStockMarketPricePort {
@Value("${alphavantage.api.key}")
private String apiKey;
@Override
public Mono<BigDecimal> get(String symbol) {
return WebClient.create().get().uri("https://www.alphavantage.co/query",
uriBuilder -> uriBuilder.queryParam("function", "TIME_SERIES_DAILY")
.queryParam("symbol", symbol)
.queryParam("apikey", apiKey)
.build()
)
.retrieve()
.bodyToMono(AlphaVantageTimeSeriesDailyJson.class)
.map(this::getLatestClosingPrice);
}
private BigDecimal getLatestClosingPrice(AlphaVantageTimeSeriesDailyJson json) {
String lastRefreshed = json.getMetaData().getLastRefreshed();
return BigDecimal.valueOf(Double.parseDouble(json.getDaily().get(lastRefreshed).getClosingPrice()));
}
}

End-to-End Test

We have now implemented all the adapters and domain services, does this work when integrated end-to-end?

Let’s write an end-to-end test to confirm this. We’ll use the @SpringBootTest annotation to link everything together.

package com.example.hexademo.api;
// imports not shown
@SpringBootTest
public class GetStockPositionAndMarketValueApiE2ETest {
@Autowired
private ApplicationContext context;
@Autowired
private StockPositionsRepository repository;
private final String user = "peterpan";
@Test
@WithMockUser(user)
void getStockPositionAndMarketValue() {
final WebTestClient client = WebTestClient.bindToApplicationContext(context).build();
// arrange
String symbol = DomainModelFaker.fakeStockSymbol();
StockPosition fakeStockPosition = DomainModelFaker.fakeStockPosition(user, symbol);
// seed database
repository.deleteAll()
.then(repository.insert(fakeStockPosition))
.block();
// act
client.get().uri("/stock-position-market-value/" + symbol)
.accept(MediaType.APPLICATION_JSON)
.exchange()
// 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()).isGreaterThan(0.0))
);
}
}

Conclusion

In this series, we explain the key concepts of the Hexagonal Architecture, and demonstrated how to implement it using TDD in Spring Boot.

You can download the full source code is in this Github repository to try it out.

There’s also a two part screencast to walk through the development process. Screencast 1 is here

Screencast 2

Back to Part 4