Implementation of REST adaptor to an external data source (Hexagonal Architecture with Spring Boot — Part 5)
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.
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